Ultrahuman: add basic support for breathing exercise

This commit is contained in:
Thomas Kuehne
2025-05-02 08:30:04 +00:00
committed by José Rebelo
parent 7019094f90
commit b6dadcb84f
14 changed files with 710 additions and 174 deletions
+4
View File
@@ -258,6 +258,10 @@
android:name=".devices.idasen.IdasenControlActivity"
android:label="@string/devicetype_idasen"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".devices.ultrahuman.UltrahumanBreathingActivity"
android:label="@string/devicetype_ultrahuma_ring_air"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.FwAppInstallerActivity"
android:label="@string/title_activity_fw_app_insaller"
@@ -0,0 +1,135 @@
/* 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.ultrahuman;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
// TODO: verify GUI lifecycle
// TODO: updates when multiple devices are active
// TODO: polish and localize the GUI
public class UltrahumanBreathingActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(UltrahumanBreathingActivity.class);
private final ExerciseUpdateReceiver UpdateReceiver = new ExerciseUpdateReceiver();
private TextView UiStatus;
private TextView UiHR;
private TextView UiHRV;
private TextView UiTemp;
private TextView UiTime;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ultrahuman_breathing);
findViewById(R.id.ultrahuman_breathing_start).setOnClickListener(new StartListener());
findViewById(R.id.ultrahuman_breathing_stop).setOnClickListener(new StopListener());
UiStatus = findViewById(R.id.ultrahuman_breathing_status);
UiHR = findViewById(R.id.ultrahuman_breathing_HR);
UiTemp = findViewById(R.id.ultrahuman_breathing_temp);
UiHRV = findViewById(R.id.ultrahuman_breathing_HRV);
UiTime = findViewById(R.id.ultrahuman_breathing_time);
IntentFilter filter = new IntentFilter();
filter.addAction(UltrahumanConstants.ACTION_EXERCISE_UPDATE);
ContextCompat.registerReceiver(getApplicationContext(), UpdateReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
changeExercise(UltrahumanExercise.CHECK);
}
private void changeExercise(UltrahumanExercise exercise) {
LOG.info("changeExercise {}", exercise);
final Intent intent = new Intent(UltrahumanConstants.ACTION_CHANGE_EXERCISE);
intent.setPackage(BuildConfig.APPLICATION_ID);
intent.putExtra(UltrahumanConstants.EXTRA_EXERCISE, exercise.Code);
getApplicationContext().sendBroadcast(intent);
}
@Override
public void onDestroy() {
getApplicationContext().unregisterReceiver(UpdateReceiver);
super.onDestroy();
}
private class StartListener implements View.OnClickListener {
@Override
public void onClick(View v) {
changeExercise(UltrahumanExercise.BREATHING_START);
}
}
private class StopListener implements View.OnClickListener {
@Override
public void onClick(View v) {
changeExercise(UltrahumanExercise.BREATHING_STOP);
}
}
private class ExerciseUpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
UltrahumanExerciseData data = (UltrahumanExerciseData) intent.getSerializableExtra(UltrahumanConstants.EXTRA_EXERCISE);
if (data != null) {
String update = String.format(Locale.US, "b:%s%% t:%x", data.BatteryLevel, 0xFF & data.Exercise);
UiStatus.setText(update);
if (data.Timestamp > -1) {
// todo - display local time
final Calendar calendar = DateTimeUtils.getCalendarUTC();
calendar.setTimeInMillis(data.Timestamp * 1000L);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
String time = String.format(Locale.US, "%02d:%02d:%02d", hour, minute, second);
UiTime.setText(time);
}
if (data.HR > -1) {
String hr = String.format(Locale.US, "%s bpm", data.HR);
UiHR.setText(hr);
}
if (data.HRV > 0) {
String hrv = String.format(Locale.US, "%s ms", data.HRV);
UiHRV.setText(hrv);
}
if (data.Temperature > -1) {
String temp = String.format(Locale.US, "%.3f °C", data.Temperature);
UiTemp.setText(temp);
}
}
}
}
}
@@ -21,21 +21,34 @@ import java.util.UUID;
public class UltrahumanConstants {
public static final String ACTION_AIRPLANE_MODE = "nodomain.freeyourgadget.gadgetbridge.ultrahuman.action.AIRPLANE_MODE";
public static final String ACTION_CHANGE_EXERCISE = "nodomain.freeyourgadget.gadgetbridge.ultrahuman.action.CHANGE_EXERCISE";
public static final String ACTION_EXERCISE_UPDATE = "nodomain.freeyourgadget.gadgetbridge.ultrahuman.action.EXERCISE_UPDATE";
public static final UUID UUID_SERVICE_COMMAND = UUID.fromString("86f65000-f706-58a0-95b2-1fb9261e4dc7");
public static final String EXTRA_ADDRESS = "address";
public static final String EXTRA_EXERCISE = "exercise";
public static final byte OPERATION_ACTIVATE_AIRPLANE_MODE = 0x70;
public static final byte OPERATION_BREATHING_START = 0x72;
public static final byte OPERATION_BREATHING_STOP = 0x74;
public static final byte OPERATION_DISABLE_POWERSAVE = (byte) 0xD1;
public static final byte OPERATION_ENABLE_POWERSAVE = (byte) 0xD2;
public static final byte OPERATION_GET_FIRST_RECORDING_NR = 0x07;
public static final byte OPERATION_GET_LAST_RECORDING_NR = 0x08;
public static final byte OPERATION_GET_RECORDINGS = 0x04;
public static final byte OPERATION_CHECK_DATA = 0x59;
public static final byte OPERATION_RESET = (byte) 0x98;
public static final byte OPERATION_SETTIME = 0x02;
public static final UUID UUID_SERVICE_DATA = UUID.fromString("86f66000-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_DATA = UUID.fromString("86f66001-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_SERVICE_REQUEST = UUID.fromString("86f65000-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_COMMAND = UUID.fromString("86f65001-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_RESPONSE = UUID.fromString("86f65002-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_SERVICE_STATE = UUID.fromString("86f61000-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_STATE = UUID.fromString("86f61001-f706-58a0-95b2-1fb9261e4dc7");
public static final byte OPERATION_SETTIME = 0x02;
public static final byte OPERATION_GET_RECORDINGS = 0x04;
public static final byte OPERATION_GET_FIRST_RECORDING_NR = 0x07;
public static final byte OPERATION_GET_LAST_RECORDING_NR = 0x08;
public static final byte OPERATION_PING = 0x59;
public static final byte OPERATION_ACTIVATE_AIRPLANE_MODE = 0x70;
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;
public static final UUID UUID_SERVICE_TODO = UUID.fromString("86f63000-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_TODO = UUID.fromString("86f63001-f706-58a0-95b2-1fb9261e4dc7");
}
@@ -23,6 +23,7 @@ import android.content.Intent;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -32,13 +33,23 @@ public class UltrahumanDeviceCardAction implements DeviceCardAction {
private final int Description;
private final int Question;
private final String Action;
private final String IntentAction;
private final Class<?> IntentClass;
public UltrahumanDeviceCardAction(int icon, int description, int question, String action) {
Icon = icon;
Description = description;
Question = question;
Action = action;
IntentAction = action;
IntentClass = null;
}
public UltrahumanDeviceCardAction(int icon, int description, Class<?> cls) {
Icon = icon;
Description = description;
Question = 0;
IntentAction = null;
IntentClass = cls;
}
@Override
@@ -53,17 +64,41 @@ public class UltrahumanDeviceCardAction implements DeviceCardAction {
@Override
public void onClick(GBDevice device, Context context) {
new MaterialAlertDialogBuilder(context)
.setTitle(Description)
.setMessage(Question)
.setIcon(Icon)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
final Intent intent = new Intent(Action);
intent.setPackage(BuildConfig.APPLICATION_ID);
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.sendBroadcast(intent);
})
.setNegativeButton(android.R.string.no, null)
.show();
if (Question != 0) {
new MaterialAlertDialogBuilder(context)
.setTitle(Description)
.setMessage(Question)
.setIcon(Icon)
.setPositiveButton(android.R.string.yes, (dialog, whichButton)
-> sendIntent(device, context))
.setNegativeButton(android.R.string.no, null)
.show();
} else {
sendIntent(device, context);
}
}
private void sendIntent(GBDevice device, Context context) {
final Intent intent = new Intent(IntentAction);
intent.setPackage(BuildConfig.APPLICATION_ID);
intent.putExtra(UltrahumanConstants.EXTRA_ADDRESS, device.getAddress());
if (IntentClass != null) {
intent.setClass(context, IntentClass);
if (AbstractGBActivity.class.isAssignableFrom(IntentClass)) {
context.startActivity(intent);
return;
}
}
context.sendBroadcast(intent);
}
@Override
public boolean isVisible(final GBDevice device) {
// device.isInitialized() also treats State.SCANNED as initialized but that isn't
// appropriate for this device type
final GBDevice.State state = device.getState();
return state.equalsOrHigherThan(GBDevice.State.INITIALIZED);
}
}
@@ -19,7 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman;
import androidx.annotation.NonNull;
import java.util.Collections;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -51,7 +51,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanDeviceStateSample
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
@@ -62,17 +61,18 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.ultrahuman.Ultrahuma
public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public List<DeviceCardAction> getCustomActions() {
// TODO - use an airplane icon instead
DeviceCardAction airplaneMode = new UltrahumanDeviceCardAction(R.drawable.ic_circle, R.string.ultrahuman_airplane_mode_title, R.string.ultrahuman_airplane_mode_question, UltrahumanConstants.ACTION_AIRPLANE_MODE);
ArrayList<DeviceCardAction> list = new ArrayList<>();
list.add(new UltrahumanDeviceCardAction(R.drawable.ic_flight, R.string.ultrahuman_airplane_mode_title, R.string.ultrahuman_airplane_mode_question, UltrahumanConstants.ACTION_AIRPLANE_MODE));
list.add(new UltrahumanDeviceCardAction(R.drawable.ic_pulmonology, R.string.ultrahuman_breathing_title, UltrahumanBreathingActivity.class));
return Collections.singletonList(airplaneMode);
return list;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
final Long deviceId = device.getId();
final Map<AbstractDao<?, ?>, Property> daoMap = new HashMap<AbstractDao<?,?>,Property>() {{
final Map<AbstractDao<?, ?>, Property> daoMap = new HashMap<AbstractDao<?, ?>, Property>() {{
put(session.getGenericHeartRateSampleDao(), GenericHeartRateSampleDao.Properties.DeviceId);
put(session.getGenericHrvValueSampleDao(), GenericHrvValueSampleDao.Properties.DeviceId);
put(session.getGenericSpo2SampleDao(), GenericSpo2SampleDao.Properties.DeviceId);
@@ -0,0 +1,30 @@
/* 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.ultrahuman;
public enum UltrahumanExercise {
CHECK((byte) 0),
BREATHING_START(UltrahumanConstants.OPERATION_BREATHING_START),
BREATHING_STOP(UltrahumanConstants.OPERATION_BREATHING_STOP);
public final byte Code;
UltrahumanExercise(byte code) {
Code = code;
}
}
@@ -0,0 +1,44 @@
/* 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.ultrahuman;
import java.io.Serializable;
public class UltrahumanExerciseData implements Serializable {
public int BatteryLevel;
public int Exercise;
public int HR;
public int HRV;
public float Temperature;
public int Timestamp;
public UltrahumanExerciseData() {
BatteryLevel = -1;
Exercise = -1;
HR = -1;
HRV = -1;
Temperature = -1;
Timestamp = -1;
}
public UltrahumanExerciseData(int batteryLevel, int exercise) {
this();
BatteryLevel = batteryLevel;
Exercise = exercise;
}
}
@@ -0,0 +1,36 @@
/* 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.service.devices.ultrahuman;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants;
public enum UltrahumanCharacteristic {
COMMAND(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND),
RESPONSE(UltrahumanConstants.UUID_CHARACTERISTIC_RESPONSE),
STATE(UltrahumanConstants.UUID_CHARACTERISTIC_STATE),
DATA(UltrahumanConstants.UUID_CHARACTERISTIC_DATA),
TODO(UltrahumanConstants.UUID_CHARACTERISTIC_TODO);
public final UUID uuid;
UltrahumanCharacteristic(UUID uuid) {
this.uuid = uuid;
}
}
@@ -17,6 +17,23 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.ultrahuman;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_ACTIVATE_AIRPLANE_MODE;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_BREATHING_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_BREATHING_STOP;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_CHECK_DATA;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_DISABLE_POWERSAVE;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_ENABLE_POWERSAVE;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_GET_FIRST_RECORDING_NR;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_GET_LAST_RECORDING_NR;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_GET_RECORDINGS;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_RESET;
import static nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants.OPERATION_SETTIME;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.ultrahuman.UltrahumanCharacteristic.COMMAND;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.ultrahuman.UltrahumanCharacteristic.DATA;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.ultrahuman.UltrahumanCharacteristic.RESPONSE;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.ultrahuman.UltrahumanCharacteristic.STATE;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.ultrahuman.UltrahumanCharacteristic.TODO;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.BroadcastReceiver;
@@ -30,9 +47,10 @@ import androidx.core.content.ContextCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
import java.util.Calendar;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
@@ -46,6 +64,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.GenericSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.GenericStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.GenericTemperatureSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanExercise;
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanExerciseData;
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.samples.UltrahumanActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.samples.UltrahumanDeviceStateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@@ -62,10 +82,13 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@@ -76,11 +99,16 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
private int FetchFrom;
private int FetchCurrent;
private int LatestBatteryLevel = -1;
private int LatestExercise = -1;
public UltrahumanDeviceSupport() {
super(LOG);
addSupportedService(UltrahumanConstants.UUID_SERVICE_COMMAND);
addSupportedService(UltrahumanConstants.UUID_SERVICE_REQUEST);
addSupportedService(UltrahumanConstants.UUID_SERVICE_STATE);
addSupportedService(UltrahumanConstants.UUID_SERVICE_DATA);
addSupportedService(UltrahumanConstants.UUID_SERVICE_TODO);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
CommandReceiver = new UltrahumanBroadcastReceiver();
@@ -106,6 +134,8 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
FetchTo = -1;
FetchFrom = -1;
FetchCurrent = -1;
LatestBatteryLevel = -1;
LatestExercise = -1;
// required for DB
if (getDevice().getFirmwareVersion() == null) {
@@ -116,9 +146,11 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
if (!CommandReceiver.Registered) {
IntentFilter filter = new IntentFilter();
filter.addAction(UltrahumanConstants.ACTION_AIRPLANE_MODE);
ContextCompat.registerReceiver(getContext(), CommandReceiver, filter, ContextCompat.RECEIVER_EXPORTED);
IntentFilter exported = new IntentFilter();
exported.addAction(UltrahumanConstants.ACTION_AIRPLANE_MODE);
exported.addAction(UltrahumanConstants.ACTION_CHANGE_EXERCISE);
ContextCompat.registerReceiver(getContext(), CommandReceiver, exported, ContextCompat.RECEIVER_EXPORTED);
CommandReceiver.Registered = true;
}
@@ -128,28 +160,26 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
builder.read(getCharacteristic(DeviceInfoProfile.UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING));
builder.read(getCharacteristic(DeviceInfoProfile.UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING));
// TODO - implement a "OPERATION_PING until answer" logic instead of waits
// sometimes the device is quite quick and other times it takes a while after
// BLE connectivity has been established before the services work reliably
builder.wait(48 * 2); //BluetoothGatt.onConnectionUpdated typically reports interval=48
builder.wait(48 * 3); //BluetoothGatt.onConnectionUpdated typically reports interval=48
builder.add(new UHRead(STATE));
builder.read(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_STATE));
builder.notify(getCharacteristic(RESPONSE.uuid), true);
builder.notify(getCharacteristic(STATE.uuid), true);
builder.notify(getCharacteristic(DATA.uuid), true);
builder.notify(getCharacteristic(TODO.uuid), true);
builder.wait(48 * 2);
builder.notify(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_RESPONSE), true);
builder.notify(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_STATE), true);
builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{UltrahumanConstants.OPERATION_PING});
builder.add(new UHWrite(COMMAND, OPERATION_CHECK_DATA));
boolean timeSync = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_TIME_SYNC, true);
if (timeSync) {
builder.add(new UltrahumanSetTimeAction(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND)));
builder.add(new UHSetTime(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 UHWrite(COMMAND, powerSave ? OPERATION_ENABLE_POWERSAVE : OPERATION_DISABLE_POWERSAVE));
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
@@ -165,22 +195,21 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
getDevice().setBusyTask(task);
getDevice().sendDeviceUpdateIntent(getContext());
TransactionBuilder builder = createTransactionBuilder("onFetchRecordedData");
buildFetchRecordedData(builder);
enqueue(builder);
}
private void buildFetchRecordedData(TransactionBuilder builder) {
// fetch deltas while the device is connected
FetchFrom = (FetchCurrent > 0) ? FetchCurrent : -1;
FetchTo = -1;
FetchCurrent = -1;
TransactionBuilder builder = createTransactionBuilder("onFetchRecordedData");
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});
if (isConnected()) {
builder.queue(getQueue());
} else {
GB.toast(getContext(), R.string.devicestatus_disconnected, Toast.LENGTH_LONG, GB.ERROR);
builder.add(new UHWrite(COMMAND, OPERATION_GET_FIRST_RECORDING_NR));
}
builder.add(new UHWrite(COMMAND, OPERATION_GET_LAST_RECORDING_NR));
}
@Override
@@ -189,9 +218,12 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
return true;
}
if (UltrahumanConstants.UUID_CHARACTERISTIC_STATE.equals(characteristic.getUuid())) {
if (STATE.uuid.equals(characteristic.getUuid())) {
final byte[] raw = characteristic.getValue();
LOG.debug("UH>>GB read {} {} {}", STATE, status, StringUtils.bytesToHex(raw));
if (status == BluetoothGatt.GATT_SUCCESS) {
return decodeDeviceState(characteristic.getValue());
return decodeDeviceState(raw);
}
}
@@ -207,11 +239,13 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
UUID characteristicUUID = characteristic.getUuid();
byte[] raw = characteristic.getValue();
if (UltrahumanConstants.UUID_CHARACTERISTIC_STATE.equals(characteristicUUID)) {
if (STATE.uuid.equals(characteristicUUID)) {
LOG.debug("UH>>GB changed STATE {}", StringUtils.bytesToHex(raw));
return decodeDeviceState(raw);
} else if (UltrahumanConstants.UUID_CHARACTERISTIC_RESPONSE.equals(characteristicUUID)) {
if (raw.length < 5) {
LOG.error("received unexpectedly short BLE command response: {}", StringUtils.bytesToHex(raw));
} else if (RESPONSE.uuid.equals(characteristicUUID)) {
LOG.debug("UH>>GB changed RESPONSE {}", StringUtils.bytesToHex(raw));
if (raw.length < 3) {
LOG.error("UH>>GB !! RESPONSE too short: {}", StringUtils.bytesToHex(raw));
return false;
}
@@ -222,26 +256,21 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
//byte chk1 = raw[raw.length-1];
//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) {
LOG.debug(info);
} else if ((0xFF & success) == 0xFF) {
LOG.warn(info);
} else {
LOG.info(info);
}
Context context = getContext();
switch (op) {
case UltrahumanConstants.OPERATION_GET_RECORDINGS:
case OPERATION_GET_RECORDINGS:
return decodeRecordings(raw);
case UltrahumanConstants.OPERATION_PING:
if (success == 0x00) {
case OPERATION_CHECK_DATA:
case OPERATION_BREATHING_START:
case OPERATION_BREATHING_STOP:
if (success == 0x00 && result == 0x01 && raw.length >= 5) {
LatestExercise = raw[3];
publishExerciseData();
return true;
}
break;
case UltrahumanConstants.OPERATION_ACTIVATE_AIRPLANE_MODE:
case OPERATION_ACTIVATE_AIRPLANE_MODE:
switch (result) {
case 0x01:
GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_activated), Toast.LENGTH_LONG, GB.INFO);
@@ -258,7 +287,7 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
GB.toast(context, airplaneMessage, Toast.LENGTH_LONG, GB.ERROR);
return false;
case UltrahumanConstants.OPERATION_GET_FIRST_RECORDING_NR:
case OPERATION_GET_FIRST_RECORDING_NR:
if (success == 0x00 && result == 0x01) {
FetchFrom = BLETypeConversions.toUint16(raw, 3);
if (FetchTo != -1) {
@@ -269,7 +298,7 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
fetchRecordedDataFinished();
break;
case UltrahumanConstants.OPERATION_GET_LAST_RECORDING_NR:
case OPERATION_GET_LAST_RECORDING_NR:
if (success == 0x00 && result == 0x01) {
FetchTo = BLETypeConversions.toUint16(raw, 3);
if (FetchFrom != -1) {
@@ -280,35 +309,108 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
fetchRecordedDataFinished();
break;
case UltrahumanConstants.OPERATION_DISABLE_POWERSAVE:
case UltrahumanConstants.OPERATION_ENABLE_POWERSAVE:
case UltrahumanConstants.OPERATION_SETTIME:
case OPERATION_DISABLE_POWERSAVE:
case OPERATION_ENABLE_POWERSAVE:
case OPERATION_SETTIME:
if (success == 0x00 && result == 0x01) {
return true;
}
break;
default:
LOG.warn("UH>>GB !! changed {} A {}", 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;
}
LOG.warn("UH>>GB !! changed {} B {}", RESPONSE, StringUtils.bytesToHex(raw));
String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, op, success, result);
GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR);
return false;
} else if (DATA.uuid.equals(characteristicUUID)) {
if (raw[0] == 0x72 && raw.length >= 27) {
LOG.debug("UH>>GB changed {} {}", DATA, StringUtils.bytesToHex(raw));
decodeBreathing(raw);
return true;
}
LOG.warn("UH>>GB !! changed {} 1 {}", DATA, StringUtils.bytesToHex(raw));
return true;
} else if (TODO.uuid.equals(characteristicUUID)) {
LOG.warn("UH>>GB !! changed {} {}", TODO, StringUtils.bytesToHex(raw));
return true;
}
LOG.error("unhandled characteristics {} - {} ", characteristicUUID, StringUtils.bytesToHex(raw));
LOG.warn("UH>>GB !! unhandled characteristic {} - {} ", characteristicUUID, StringUtils.bytesToHex(raw));
return false;
}
private void decodeBreathing(byte[] raw) {
char[] undecoded = StringUtils.bytesToHex(raw).toCharArray();
UltrahumanExerciseData data = new UltrahumanExerciseData(LatestBatteryLevel, LatestExercise);
data.Timestamp = BLETypeConversions.toUint32(raw, 3);
undecoded[3 * 2] = undecoded[3 * 2 + 1] = '.';
undecoded[4 * 2] = undecoded[4 * 2 + 1] = '.';
undecoded[5 * 2] = undecoded[5 * 2 + 1] = '.';
undecoded[6 * 2] = undecoded[6 * 2 + 1] = '.';
data.HR = BLETypeConversions.toUnsigned(raw, 7);
undecoded[7 * 2] = undecoded[7 * 2 + 1] = '.';
data.HRV = BLETypeConversions.toUnsigned(raw, 9);
undecoded[9 * 2] = undecoded[9 * 2 + 1] = '.';
data.Temperature = Float.intBitsToFloat(BLETypeConversions.toUint32(raw, 11));
undecoded[11 * 2] = undecoded[11 * 2 + 1] = '.';
undecoded[12 * 2] = undecoded[12 * 2 + 1] = '.';
undecoded[13 * 2] = undecoded[13 * 2 + 1] = '.';
undecoded[14 * 2] = undecoded[14 * 2 + 1] = '.';
// type
undecoded[0] = undecoded[1] = '.';
// payload length
undecoded[2] = undecoded[3] = '.';
undecoded[4] = undecoded[5] = '.';
// check sum
undecoded[undecoded.length - 4] = '.';
undecoded[undecoded.length - 3] = '.';
undecoded[undecoded.length - 2] = '.';
undecoded[undecoded.length - 1] = '.';
LOG.debug("UH>>GB decode DAT72 {}", new String(undecoded));
publishExerciseData(data);
}
private void publishExerciseData() {
if (LatestExercise > -1 && LatestBatteryLevel > -1) {
UltrahumanExerciseData data = new UltrahumanExerciseData(LatestBatteryLevel, LatestExercise);
publishExerciseData(data);
}
}
private void publishExerciseData(UltrahumanExerciseData data) {
final Intent intent = new Intent(UltrahumanConstants.ACTION_EXERCISE_UPDATE);
intent.setPackage(BuildConfig.APPLICATION_ID);
String address = getDevice().getAddress();
intent.putExtra(UltrahumanConstants.EXTRA_ADDRESS, address);
intent.putExtra(UltrahumanConstants.EXTRA_EXERCISE, data);
final Context context = getContext();
context.sendBroadcast(intent);
}
@Override
public void onSendConfiguration(String config) {
if (DeviceSettingsPreferenceConst.PREF_TIME_SYNC.equals(config)) {
onSetTime();
} else if (DeviceSettingsPreferenceConst.PREF_POWER_SAVING.equals(config)) {
onSetPowerSaveMode();
boolean powerSave = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_POWER_SAVING, true);
sendCommand("onSetPowerSaveMode", powerSave ? OPERATION_ENABLE_POWERSAVE : OPERATION_DISABLE_POWERSAVE);
} else {
super.onSendConfiguration(config);
}
@@ -324,13 +426,13 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
if (FetchFrom > FetchTo) {
FetchFrom = 0;
}
sendCommand("GetRecordingsCommand", new byte[]{UltrahumanConstants.OPERATION_GET_RECORDINGS, (byte) (FetchFrom & 0xFF), (byte) ((FetchFrom >> 8) & 0xFF)});
sendCommand("fetchRecordingActually", OPERATION_GET_RECORDINGS, (byte) (FetchFrom & 0xFF), (byte) ((FetchFrom >> 8) & 0xFF));
}
private boolean decodeRecordings(byte[] raw) {
if (raw[1] != 0) {
if ((raw[1] & 0xFF) == 0xEE) {
LOG.warn("no historic data recorded");
LOG.warn("!! no historic data recorded");
} else {
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);
@@ -377,9 +479,9 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
float temperatureMin = Float.intBitsToFloat(BLETypeConversions.toUint32(raw, start + 16));
int timestampActivity = BLETypeConversions.toUint32(raw, start + 20);
int rawIntensity = BLETypeConversions.toUint16(raw[start + 24]);
int steps = BLETypeConversions.toUint16(raw[start + 26]);
int stress = (BLETypeConversions.toUint16(raw[start + 28]) * 100) / 255;
int rawIntensity = BLETypeConversions.toUnsigned(raw[start + 24]);
int steps = BLETypeConversions.toUint16(raw, start + 26);
int stress = (BLETypeConversions.toUnsigned(raw[start + 28]) * 100) / 255;
int index = BLETypeConversions.toUint16(raw, start + 30);
@@ -449,12 +551,22 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
try (DBHandler db = GBApplication.acquireDB()) {
if (raw.length != 7) {
LOG.warn("received Device State with unexpected length {}: {}", raw.length, StringUtils.bytesToHex(raw));
LOG.warn("!! received Device State with unexpected length {}: {}", raw.length, StringUtils.bytesToHex(raw));
} else {
char[] undecoded = StringUtils.bytesToHex(raw).toCharArray();
batteryLevel = 0xFF & raw[0];
undecoded[0] = undecoded[1] = '.';
// decoding of 1..4 is unknown
deviceState = 0xFF & raw[5];
undecoded[5 * 2] = undecoded[5 * 2 + 1] = '.';
deviceTemperature = 0xFF & raw[6];
undecoded[6 * 2] = undecoded[6 * 2 + 1] = '.';
LOG.debug("UH>>GB decode STATE {}", new String(undecoded));
switch (deviceState) {
case 0x00:
@@ -464,7 +576,7 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
batteryState = (batteryLevel > 99) ? BatteryState.BATTERY_CHARGING_FULL : BatteryState.BATTERY_CHARGING;
break;
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));
}
}
@@ -490,11 +602,15 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
batteryEvent.state = batteryState;
evaluateGBDeviceEvent(batteryEvent);
if (batteryEvent.level != LatestBatteryLevel) {
LatestBatteryLevel = batteryEvent.level;
publishExerciseData();
}
return success;
}
private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) {
LOG.debug("handleDeviceInfo: {}", info);
GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
versionCmd.fwVersion = info.getFirmwareRevision();
versionCmd.fwVersion2 = info.getSerialNumber();
@@ -504,32 +620,15 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
private void fetchRecordedDataFinished() {
GB.updateTransferNotification(null, "", false, 100, getContext());
LOG.info("Sync finished!");
getDevice().unsetBusyTask();
getDevice().sendDeviceUpdateIntent(getContext());
GB.signalActivityDataFinish(getDevice());
}
private void activateAirplaneMode() {
sendCommand("activateAirplaneMode", new byte[]{UltrahumanConstants.OPERATION_ACTIVATE_AIRPLANE_MODE});
}
@Override
public void onReset(int flags) {
if ((flags & GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET) == GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET) {
sendCommand("onReset", new byte[]{UltrahumanConstants.OPERATION_RESET});
}
}
private void onSetPowerSaveMode() {
boolean powerSave = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_POWER_SAVING, true);
TransactionBuilder builder = createTransactionBuilder("onSetPowerSaveMode");
builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{powerSave ? UltrahumanConstants.OPERATION_ENABLE_POWERSAVE : UltrahumanConstants.OPERATION_DISABLE_POWERSAVE});
if (isConnected()) {
builder.queue(getQueue());
} else {
GB.toast(getContext(), R.string.devicestatus_disconnected, Toast.LENGTH_LONG, GB.ERROR);
sendCommand("onReset", OPERATION_RESET);
}
}
@@ -538,22 +637,30 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
boolean timeSync = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_TIME_SYNC, true);
if (timeSync) {
TransactionBuilder builder = createTransactionBuilder("onSetTime");
UltrahumanSetTimeAction action = new UltrahumanSetTimeAction(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND));
builder.add(action);
builder.add(new UHSetTime(COMMAND));
if (isConnected()) {
builder.queue(getQueue());
} else {
GB.toast(getContext(), R.string.devicestatus_disconnected, Toast.LENGTH_LONG, GB.ERROR);
}
enqueue(builder);
}
}
private void sendCommand(String taskName, byte[] contents) {
LOG.debug("sendCommand {} : {}", taskName, StringUtils.bytesToHex(contents));
private void sendCommand(String taskName, byte... contents) {
TransactionBuilder builder = createTransactionBuilder(taskName);
builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), contents);
builder.add(new UHWrite(COMMAND, contents));
enqueue(builder);
}
private void changeExercise(byte exercise) {
TransactionBuilder builder = createTransactionBuilder("changeExercise");
if (exercise != UltrahumanExercise.CHECK.Code) {
builder.add(new UHWrite(COMMAND, exercise));
}
builder.add(new UHWrite(COMMAND, OPERATION_CHECK_DATA));
enqueue(builder);
}
private void enqueue(final TransactionBuilder builder) {
if (isConnected()) {
builder.queue(getQueue());
} else {
@@ -561,6 +668,21 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
}
}
private BluetoothGattCharacteristic resolve(UltrahumanCharacteristic chara) {
switch (chara) {
case DATA:
return getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_DATA);
case STATE:
return getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_STATE);
case COMMAND:
return getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND);
case RESPONSE:
return getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_RESPONSE);
}
LOG.error("resolve {} is unknown", chara);
return null;
}
private class UltrahumanBroadcastReceiver extends BroadcastReceiver implements IntentListener {
boolean Registered;
@@ -571,18 +693,87 @@ public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport {
@Override
public void notify(Intent intent) {
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device != null && !device.equals(getDevice())) {
final String address = intent.getStringExtra(UltrahumanConstants.EXTRA_ADDRESS);
if (address != null && !address.isEmpty() && !address.equalsIgnoreCase(getDevice().getAddress())) {
// this intent is for another device
return;
}
String action = intent.getAction();
if (UltrahumanConstants.ACTION_AIRPLANE_MODE.equals(action)) {
activateAirplaneMode();
} else if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) {
handleDeviceInfo(intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
final String action = intent.getAction();
if (action == null) {
return;
}
switch (action) {
case UltrahumanConstants.ACTION_AIRPLANE_MODE:
sendCommand("activateAirplaneMode", OPERATION_ACTIVATE_AIRPLANE_MODE);
return;
case UltrahumanConstants.ACTION_CHANGE_EXERCISE:
final byte exercise = intent.getByteExtra(UltrahumanConstants.EXTRA_EXERCISE, UltrahumanExercise.CHECK.Code);
changeExercise(exercise);
return;
default:
if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) {
handleDeviceInfo(intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
return;
}
}
}
}
/// simplify debugging and tracing
private class UHWrite extends WriteAction {
private final UltrahumanCharacteristic Characteristic;
public UHWrite(UltrahumanCharacteristic chara, byte... value) {
super(resolve(chara), value);
Characteristic = chara;
}
@Override
protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
LOG.debug("GB>>UH write {} {}", Characteristic, StringUtils.bytesToHex(value));
return super.writeValue(gatt, characteristic, value);
}
}
/// simplify debugging and tracing
private class UHRead extends ReadAction {
private final UltrahumanCharacteristic Characteristic;
public UHRead(UltrahumanCharacteristic chara) {
super(resolve(chara));
Characteristic = chara;
}
@Override
public boolean run(BluetoothGatt gatt) {
LOG.debug("GB>>UH read {}", Characteristic);
return super.run(gatt);
}
}
/// calculate date/time on the fly to avoid setting an outdated value
private class UHSetTime extends UHWrite {
UHSetTime(UltrahumanCharacteristic chara) {
super(chara);
}
@Override
protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
final Calendar calendar = DateTimeUtils.getCalendarUTC();
final long millis = calendar.getTimeInMillis();
final long epoc = Math.round(millis / 1000.0d);
byte[] command = new byte[]{
OPERATION_SETTIME,
(byte) (epoc & 0xff),
(byte) ((epoc >> 8) & 0xff),
(byte) ((epoc >> 16) & 0xff),
(byte) ((epoc >> 24) & 0xff)
};
return super.writeValue(gatt, characteristic, command);
}
}
}
@@ -1,50 +0,0 @@
/* 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.service.devices.ultrahuman;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.Calendar;
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
/// calculate date/time on the fly to avoid setting an outdated value
class UltrahumanSetTimeAction extends WriteAction {
UltrahumanSetTimeAction(BluetoothGattCharacteristic characteristic) {
super(characteristic, null);
}
@Override
protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
final Calendar calendar = DateTimeUtils.getCalendarUTC();
final long millis = calendar.getTimeInMillis();
final long epoc = Math.round(millis / 1000.0d);
byte[] command = new byte[]{
UltrahumanConstants.OPERATION_SETTIME,
(byte) (epoc & 0xff),
(byte) ((epoc >> 8) & 0xff),
(byte) ((epoc >> 16) & 0xff),
(byte) ((epoc >> 24) & 0xff)
};
return super.writeValue(gatt, characteristic, command);
}
}
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,843L376,873Q366,876 358,869.5Q350,863 350,853Q350,844 352,839.5Q354,835 358,832L430,778L430,527L116,620Q102,624 91,615.5Q80,607 80,593Q80,582 84,575Q88,568 94,564L430,366L430,130Q430,109 444.5,94.5Q459,80 480,80Q501,80 515.5,94.5Q530,109 530,130L530,366L866,564Q872,568 876,575Q880,582 880,593Q880,607 869,615.5Q858,624 844,620L530,527L530,778L602,832Q606,835 608,839.5Q610,844 610,853Q610,863 602,869.5Q594,876 584,873L480,843Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M190,840Q144,840 112,808Q80,776 80,730L80,556Q80,551 81,545.5Q82,540 84,535L188,266Q200,235 228,217.5Q256,200 289,200Q331,200 360.5,229.5Q390,259 390,301L390,362L312,440Q303,449 303.5,461Q304,473 313,482Q322,491 334.5,491Q347,491 356,482L450,388L450,110Q450,97 458.5,88.5Q467,80 480,80Q493,80 501.5,88.5Q510,97 510,110L510,388L605,483Q614,492 626.5,491.5Q639,491 648,482Q657,473 657,460.5Q657,448 648,439L570,362L570,301Q570,259 599.5,229.5Q629,200 671,200Q704,200 732,217.5Q760,235 772,266L876,535Q878,540 879,545.5Q880,551 880,556L880,730Q880,776 848,808Q816,840 770,840L630,840Q584,840 552,808Q520,776 520,730L520,664Q520,662 520.5,660Q521,658 521,656L541,504L480,442L418,504L439,656Q439,658 439.5,660Q440,662 440,664L440,730Q440,776 408,808Q376,840 330,840L190,840Z"/>
</vector>
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/ultrahuman_breathing_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="b:000% t:00"
android:textAlignment="center" />
<TextView
android:id="@+id/ultrahuman_breathing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="00:00:00"
android:textAlignment="center" />
<TextView
android:id="@+id/ultrahuman_breathing_HR"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="000 bpm"
android:textAlignment="center" />
<TextView
android:id="@+id/ultrahuman_breathing_temp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="00.000 °C"
android:textAlignment="center" />
<TextView
android:id="@+id/ultrahuman_breathing_HRV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="000 ms"
android:textAlignment="center" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:orientation="horizontal">
<Button
android:id="@+id/ultrahuman_breathing_start"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="@string/ultrahuman_breathing_start" />
<Button
android:id="@+id/ultrahuman_breathing_stop"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="@string/ultrahuman_breathing_stop" />
</LinearLayout>
</LinearLayout>
+3
View File
@@ -3890,6 +3890,9 @@
<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_title">airplane mode</string>
<string name="ultrahuman_breathing_title">Breathing</string>
<string name="ultrahuman_breathing_start">Start</string>
<string name="ultrahuman_breathing_stop">Stop</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>