mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Ultrahuman: add basic support for breathing exercise
This commit is contained in:
committed by
José Rebelo
parent
7019094f90
commit
b6dadcb84f
@@ -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"
|
||||
|
||||
+135
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-10
@@ -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");
|
||||
}
|
||||
|
||||
+49
-14
@@ -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);
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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);
|
||||
|
||||
+30
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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;
|
||||
}
|
||||
}
|
||||
+36
@@ -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;
|
||||
}
|
||||
}
|
||||
+285
-94
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-50
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user