mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Zepp OS: Extract heart rate and steps to standalone services
This commit is contained in:
+5
-1
@@ -36,7 +36,11 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepState;
|
||||
public class GBDeviceEventSleepStateDetection extends GBDeviceEvent {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GBDeviceEventSleepStateDetection.class);
|
||||
|
||||
public SleepState sleepState;
|
||||
public final SleepState sleepState;
|
||||
|
||||
public GBDeviceEventSleepStateDetection(final SleepState sleepState) {
|
||||
this.sleepState = sleepState;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
+5
-1
@@ -36,7 +36,11 @@ import nodomain.freeyourgadget.gadgetbridge.model.WearingState;
|
||||
public class GBDeviceEventWearState extends GBDeviceEvent {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GBDeviceEventWearState.class);
|
||||
|
||||
public WearingState wearingState = WearingState.UNKNOWN;
|
||||
public final WearingState wearingState;
|
||||
|
||||
public GBDeviceEventWearState(final WearingState wearingState) {
|
||||
this.wearingState = wearingState;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
-29
@@ -17,35 +17,6 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huami;
|
||||
|
||||
public class Huami2021Service {
|
||||
/**
|
||||
* Endpoints for 2021 chunked protocol
|
||||
*/
|
||||
public static final short CHUNKED2021_ENDPOINT_STEPS = 0x0016;
|
||||
public static final short CHUNKED2021_ENDPOINT_HEARTRATE = 0x001d;
|
||||
public static final short CHUNKED2021_ENDPOINT_AUTH = 0x0082;
|
||||
public static final short CHUNKED2021_ENDPOINT_COMPAT = 0x0090;
|
||||
|
||||
/**
|
||||
* Steps, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_STEPS}.
|
||||
*/
|
||||
public static final byte STEPS_CMD_GET = 0x03;
|
||||
public static final byte STEPS_CMD_REPLY = 0x04;
|
||||
public static final byte STEPS_CMD_ENABLE_REALTIME = 0x05;
|
||||
public static final byte STEPS_CMD_ENABLE_REALTIME_ACK = 0x06;
|
||||
public static final byte STEPS_CMD_REALTIME_NOTIFICATION = 0x07;
|
||||
|
||||
/**
|
||||
* Notifications, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_HEARTRATE}.
|
||||
*/
|
||||
public static final byte HEART_RATE_CMD_REALTIME_SET = 0x04;
|
||||
public static final byte HEART_RATE_CMD_REALTIME_ACK = 0x05;
|
||||
public static final byte HEART_RATE_CMD_SLEEP = 0x06;
|
||||
public static final byte HEART_RATE_FALL_ASLEEP = 0x01;
|
||||
public static final byte HEART_RATE_WAKE_UP = 0x00;
|
||||
public static final byte HEART_RATE_REALTIME_MODE_STOP = 0x00;
|
||||
public static final byte HEART_RATE_REALTIME_MODE_START = 0x01;
|
||||
public static final byte HEART_RATE_REALTIME_MODE_CONTINUE = 0x02;
|
||||
|
||||
/**
|
||||
* Raw sensor control.
|
||||
*/
|
||||
|
||||
+1
-2
@@ -148,8 +148,7 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
|
||||
|
||||
@Override
|
||||
public boolean supportsManualHeartRateMeasurement(final GBDevice device) {
|
||||
// TODO: It should be supported, but not yet properly implemented
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -153,11 +153,6 @@ public interface DeviceService extends EventHandler {
|
||||
String EXTRA_CAMERA_EVENT = "event";
|
||||
String EXTRA_CAMERA_FILENAME = "filename";
|
||||
|
||||
/**
|
||||
* Use EXTRA_REALTIME_SAMPLE instead
|
||||
*/
|
||||
@Deprecated
|
||||
String EXTRA_REALTIME_STEPS = "realtime_steps";
|
||||
String EXTRA_REALTIME_SAMPLE = "realtime_sample";
|
||||
String EXTRA_TIMESTAMP = "timestamp";
|
||||
/**
|
||||
|
||||
+9
-33
@@ -73,7 +73,6 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
|
||||
@@ -88,7 +87,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.ActivateDisplayOnLift;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.ActivateDisplayOnLiftSensitivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.DisconnectNotificationSetting;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
|
||||
@@ -272,6 +270,8 @@ import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacterist
|
||||
public abstract class HuamiSupport extends AbstractBTLEDeviceSupport
|
||||
implements Huami2021Handler, HuamiFetcher.HuamiFetchSupport {
|
||||
|
||||
public static final short CHUNKED2021_ENDPOINT_COMPAT = 0x0090;
|
||||
|
||||
// We introduce key press counter for notification purposes
|
||||
private static int currentButtonActionId = 0;
|
||||
private static int currentButtonPressCount = 0;
|
||||
@@ -1693,7 +1693,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport
|
||||
break;
|
||||
case HuamiDeviceEvent.START_NONWEAR:
|
||||
LOG.info("non-wear start detected");
|
||||
processDeviceEvent(HuamiDeviceEvent.START_NONWEAR);
|
||||
evaluateGBDeviceEvent(new GBDeviceEventWearState(WearingState.NOT_WEARING));
|
||||
break;
|
||||
case HuamiDeviceEvent.ALARM_TOGGLED:
|
||||
case HuamiDeviceEvent.ALARM_CHANGED:
|
||||
@@ -1704,11 +1704,11 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport
|
||||
break;
|
||||
case HuamiDeviceEvent.FELL_ASLEEP:
|
||||
LOG.info("Fell asleep");
|
||||
processDeviceEvent(HuamiDeviceEvent.FELL_ASLEEP);
|
||||
evaluateGBDeviceEvent(new GBDeviceEventSleepStateDetection(SleepState.ASLEEP));
|
||||
break;
|
||||
case HuamiDeviceEvent.WOKE_UP:
|
||||
LOG.info("Woke up");
|
||||
processDeviceEvent(HuamiDeviceEvent.WOKE_UP);
|
||||
evaluateGBDeviceEvent(new GBDeviceEventSleepStateDetection(SleepState.AWAKE));
|
||||
break;
|
||||
case HuamiDeviceEvent.STEPSGOAL_REACHED:
|
||||
LOG.info("Steps goal reached");
|
||||
@@ -1966,30 +1966,6 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport
|
||||
writeToConfiguration(builder, cmd);
|
||||
}
|
||||
|
||||
protected void processDeviceEvent(int deviceEvent){
|
||||
LOG.debug("Handling device event: " + deviceEvent);
|
||||
GBDeviceEvent event;
|
||||
switch (deviceEvent) {
|
||||
case HuamiDeviceEvent.WOKE_UP:
|
||||
event = new GBDeviceEventSleepStateDetection();
|
||||
((GBDeviceEventSleepStateDetection) event).sleepState = SleepState.AWAKE;
|
||||
break;
|
||||
case HuamiDeviceEvent.FELL_ASLEEP:
|
||||
event = new GBDeviceEventSleepStateDetection();
|
||||
((GBDeviceEventSleepStateDetection) event).sleepState = SleepState.ASLEEP;
|
||||
break;
|
||||
case HuamiDeviceEvent.START_NONWEAR:
|
||||
event = new GBDeviceEventWearState();
|
||||
((GBDeviceEventWearState) event).wearingState = WearingState.NOT_WEARING;
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unhandled device event {}", deviceEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
evaluateGBDeviceEvent(event);
|
||||
}
|
||||
|
||||
private void handleLongButtonEvent(){
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
|
||||
@@ -2176,7 +2152,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleRealtimeSteps(byte[] value) {
|
||||
private void handleRealtimeSteps(byte[] value) {
|
||||
if (value == null) {
|
||||
LOG.error("realtime steps: value is null");
|
||||
return;
|
||||
@@ -3794,7 +3770,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport
|
||||
}
|
||||
|
||||
byte[] command = ArrayUtils.addAll(new byte[]{0x00, 0x00, (byte) (0xc0 | type), 0x00}, data);
|
||||
writeToChunked2021(builder, Huami2021Service.CHUNKED2021_ENDPOINT_COMPAT, command, encrypt);
|
||||
writeToChunked2021(builder, CHUNKED2021_ENDPOINT_COMPAT, command, encrypt);
|
||||
} else {
|
||||
writeToChunkedOld(builder, type, data);
|
||||
}
|
||||
@@ -3853,7 +3829,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport
|
||||
public void writeToConfiguration(TransactionBuilder builder, byte[] data) {
|
||||
if (force2021Protocol()) {
|
||||
data = ArrayUtils.insert(0, data, (byte) 1);
|
||||
writeToChunked2021(builder, Huami2021Service.CHUNKED2021_ENDPOINT_COMPAT, data, true);
|
||||
writeToChunked2021(builder, CHUNKED2021_ENDPOINT_COMPAT, data, true);
|
||||
} else {
|
||||
builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), data);
|
||||
}
|
||||
@@ -4015,7 +3991,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport
|
||||
|
||||
@Override
|
||||
public void handle2021Payload(short type, byte[] payload) {
|
||||
if (type == Huami2021Service.CHUNKED2021_ENDPOINT_COMPAT) {
|
||||
if (type == CHUNKED2021_ENDPOINT_COMPAT) {
|
||||
LOG.info("got configuration data");
|
||||
type = 0;
|
||||
handleConfigurationInfo(ArrayUtils.remove(payload, 0));
|
||||
|
||||
+5
-4
@@ -32,7 +32,6 @@ import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
@@ -49,6 +48,8 @@ public class InitOperation2021 extends InitOperation implements Huami2021Handler
|
||||
private final byte[] privateEC = new byte[24];
|
||||
private byte[] publicEC;
|
||||
|
||||
public static final short CHUNKED2021_ENDPOINT_AUTH = 0x0082;
|
||||
|
||||
private final BluetoothGattCharacteristic characteristicChunked2021Write;
|
||||
private final Huami2021ChunkedEncoder huami2021ChunkedEncoder;
|
||||
private final Huami2021ChunkedDecoder huami2021ChunkedDecoder;
|
||||
@@ -82,7 +83,7 @@ public class InitOperation2021 extends InitOperation implements Huami2021Handler
|
||||
sendPubKeyCommand[2] = 0x00;
|
||||
sendPubKeyCommand[3] = 0x02;
|
||||
System.arraycopy(publicEC, 0, sendPubKeyCommand, 4, 48);
|
||||
huami2021ChunkedEncoder.write(chunk -> builder.write(characteristicChunked2021Write, chunk), Huami2021Service.CHUNKED2021_ENDPOINT_AUTH, sendPubKeyCommand, true, false);
|
||||
huami2021ChunkedEncoder.write(chunk -> builder.write(characteristicChunked2021Write, chunk), CHUNKED2021_ENDPOINT_AUTH, sendPubKeyCommand, true, false);
|
||||
}
|
||||
|
||||
private void generateKeyPair() {
|
||||
@@ -115,7 +116,7 @@ public class InitOperation2021 extends InitOperation implements Huami2021Handler
|
||||
|
||||
@Override
|
||||
public void handle2021Payload(final short type, final byte[] payload) {
|
||||
if (type != Huami2021Service.CHUNKED2021_ENDPOINT_AUTH) {
|
||||
if (type != CHUNKED2021_ENDPOINT_AUTH) {
|
||||
this.huamiSupport.handle2021Payload(type, payload);
|
||||
return;
|
||||
}
|
||||
@@ -151,7 +152,7 @@ public class InitOperation2021 extends InitOperation implements Huami2021Handler
|
||||
System.arraycopy(encryptedRandom1, 0, command, 1, 16);
|
||||
System.arraycopy(encryptedRandom2, 0, command, 17, 16);
|
||||
TransactionBuilder builder = createTransactionBuilder("Sending double encryted random to device");
|
||||
huami2021ChunkedEncoder.write(chunk -> builder.write(characteristicChunked2021Write, chunk), Huami2021Service.CHUNKED2021_ENDPOINT_AUTH, command, true, false);
|
||||
huami2021ChunkedEncoder.write(chunk -> builder.write(characteristicChunked2021Write, chunk), CHUNKED2021_ENDPOINT_AUTH, command, true, false);
|
||||
huamiSupport.performImmediately(builder);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
|
||||
+28
-151
@@ -16,9 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos;
|
||||
|
||||
import static org.apache.commons.lang3.ArrayUtils.subarray;
|
||||
import static java.lang.Thread.sleep;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service.*;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions.fromUint16;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions.fromUint8;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions.mapTimeZone;
|
||||
@@ -27,6 +25,7 @@ import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService.ConfigArg.LANGUAGE_FOLLOW_PHONE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService.ConfigArg.SLEEP_HIGH_ACCURACY_MONITORING;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Context;
|
||||
@@ -102,7 +101,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ChunkedDecoder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ChunkedEncoder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsFirmwareUpdateOperation;
|
||||
@@ -120,6 +118,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConnectionService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsDisplayItemsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFindDeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsHeartRateService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsHttpService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLoyaltyCardService;
|
||||
@@ -136,6 +135,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsMorningUpdatesService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsPhoneService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsSilentModeService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsStepsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsUserInfoService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsVibrationPatternsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsVoiceMemosService;
|
||||
@@ -148,17 +148,15 @@ import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.RealtimeSamplesAggregator;
|
||||
|
||||
public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferService.DownloadCallback {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsSupport.class);
|
||||
|
||||
// Tracks whether realtime HR monitoring is already started, so we can just
|
||||
// send CONTINUE commands
|
||||
private boolean heartRateRealtimeStarted;
|
||||
private ScheduledExecutorService heartRateRealtimeScheduler;
|
||||
// Keep track of whether the rawSensor is enabled
|
||||
private boolean rawSensor = false;
|
||||
private ScheduledExecutorService rawSensorScheduler;
|
||||
private RealtimeSamplesAggregator realtimeSamplesAggregator;
|
||||
|
||||
// Services
|
||||
private final ZeppOsServicesService servicesService = new ZeppOsServicesService(this);
|
||||
@@ -197,11 +195,11 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
private final ZeppOsConnectionService connectionService = new ZeppOsConnectionService(this);
|
||||
private final ZeppOsWorldClocksService worldClocksService = new ZeppOsWorldClocksService(this);
|
||||
private final ZeppOsWorkoutService workoutService = new ZeppOsWorkoutService(this);
|
||||
private final ZeppOsHeartRateService heartRateService = new ZeppOsHeartRateService(this);
|
||||
private final ZeppOsStepsService stepsService = new ZeppOsStepsService(this);
|
||||
|
||||
private final Set<Short> mSupportedServices = new HashSet<>();
|
||||
// FIXME: We need to keep track of which services are encrypted for now, since not all of them were yet migrated to a service
|
||||
private final Set<Short> mIsEncrypted = new HashSet<>();
|
||||
private final Map<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<Short, AbstractZeppOsService>() {{
|
||||
private final Map<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<>() {{
|
||||
put(servicesService.getEndpoint(), servicesService);
|
||||
put(authenticationService.getEndpoint(), authenticationService);
|
||||
put(batteryService.getEndpoint(), batteryService);
|
||||
@@ -238,6 +236,8 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
put(weatherService.getEndpoint(), weatherService);
|
||||
put(worldClocksService.getEndpoint(), worldClocksService);
|
||||
put(workoutService.getEndpoint(), workoutService);
|
||||
put(heartRateService.getEndpoint(), heartRateService);
|
||||
put(stepsService.getEndpoint(), stepsService);
|
||||
}};
|
||||
|
||||
public ZeppOsSupport() {
|
||||
@@ -248,6 +248,15 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
super(logger);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(final GBDevice gbDevice, final BluetoothAdapter btAdapter, final Context context) {
|
||||
super.setContext(gbDevice, btAdapter, context);
|
||||
heartRateService.setSleepAsAndroidSender(sleepAsAndroidSender);
|
||||
realtimeSamplesAggregator = new RealtimeSamplesAggregator(getContext(), getDevice());
|
||||
heartRateService.setRealtimeSamplesAggregator(realtimeSamplesAggregator);
|
||||
stepsService.setRealtimeSamplesAggregator(realtimeSamplesAggregator);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte getAuthFlags() {
|
||||
return 0x00;
|
||||
@@ -390,41 +399,12 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
// TODO onHeartRateTest - what modes? this only works sometimes
|
||||
|
||||
try {
|
||||
final TransactionBuilder builder = performInitialized("HeartRateTest");
|
||||
enableNotifyHeartRateMeasurements(true, builder);
|
||||
//writeToChunked2021(builder, CHUNKED2021_ENDPOINT_HEARTRATE, new byte[]{HEART_RATE_CMD_REALTIME_SET, HEART_RATE_REALTIME_MODE_START}, false);
|
||||
builder.queue(getQueue());
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Unable to read heart rate from Huami 2021 device", e);
|
||||
}
|
||||
heartRateService.onHeartRateTest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeHeartRateMeasurement(final boolean enable) {
|
||||
final byte hrcmd;
|
||||
if (!enable) {
|
||||
hrcmd = HEART_RATE_REALTIME_MODE_STOP;
|
||||
} else if (heartRateRealtimeStarted == enable) {
|
||||
hrcmd = HEART_RATE_REALTIME_MODE_CONTINUE;
|
||||
} else {
|
||||
// enable == true, for the first time
|
||||
hrcmd = HEART_RATE_REALTIME_MODE_START;
|
||||
}
|
||||
|
||||
heartRateRealtimeStarted = enable;
|
||||
|
||||
try {
|
||||
final TransactionBuilder builder = performInitialized("Set realtime heart rate measurement = " + enable);
|
||||
enableNotifyHeartRateMeasurements(enable, builder);
|
||||
writeToChunked2021(builder, CHUNKED2021_ENDPOINT_HEARTRATE, new byte[]{HEART_RATE_CMD_REALTIME_SET, hrcmd}, false);
|
||||
builder.queue(getQueue());
|
||||
enableRealtimeSamplesTimer(enable);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Unable to set realtime heart rate measurement", e);
|
||||
}
|
||||
heartRateService.onEnableRealtimeHeartRateMeasurement(enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -497,9 +477,7 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeSteps(final boolean enable) {
|
||||
final byte[] cmd = {STEPS_CMD_ENABLE_REALTIME, bool(enable)};
|
||||
|
||||
writeToChunked2021("toggle realtime steps", CHUNKED2021_ENDPOINT_STEPS, cmd, false);
|
||||
stepsService.onEnableRealtimeSteps(enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -779,13 +757,13 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
break;
|
||||
// Received when the app starts sleep tracking
|
||||
case SleepAsAndroidAction.START_TRACKING:
|
||||
enableRealtimeHeartRateMeasurement(true);
|
||||
heartRateService.onEnableRealtimeHeartRateMeasurement(true);
|
||||
enableRawSensor(true);
|
||||
sleepAsAndroidSender.startTracking();
|
||||
break;
|
||||
// Received when the app stops sleep tracking
|
||||
case SleepAsAndroidAction.STOP_TRACKING:
|
||||
enableRealtimeHeartRateMeasurement(false);
|
||||
heartRateService.onEnableRealtimeHeartRateMeasurement(false);
|
||||
enableRawSensor(false);
|
||||
sleepAsAndroidSender.stopTracking();
|
||||
break;
|
||||
@@ -864,37 +842,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
GBApplication.deviceService(gbDevice).onSetAlarms(alarms);
|
||||
}
|
||||
|
||||
private ScheduledExecutorService startRealtimeHeartRateMeasurement() {
|
||||
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
|
||||
service.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (heartRateRealtimeStarted) {
|
||||
onEnableRealtimeHeartRateMeasurement(true);
|
||||
}
|
||||
}
|
||||
}, 0, 1000, TimeUnit.MILLISECONDS);
|
||||
return service;
|
||||
}
|
||||
|
||||
private void stopRealtimeHeartRateMeasurement() {
|
||||
if (heartRateRealtimeScheduler != null) {
|
||||
heartRateRealtimeScheduler.shutdown();
|
||||
heartRateRealtimeScheduler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void enableRealtimeHeartRateMeasurement(boolean enable) {
|
||||
onEnableRealtimeHeartRateMeasurement(enable);
|
||||
if (enable) {
|
||||
heartRateRealtimeScheduler = startRealtimeHeartRateMeasurement();
|
||||
}
|
||||
else {
|
||||
stopRealtimeHeartRateMeasurement();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void stopRawSensors() {
|
||||
if (rawSensorScheduler != null) {
|
||||
rawSensorScheduler.shutdown();
|
||||
@@ -953,13 +900,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
LOG.warn("writeToChunkedOld is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToChunked2021(final TransactionBuilder builder, final short endpoint, final byte[] data, final boolean encryptIgnored) {
|
||||
// Ensure communication for all services contains the encrypted flag reported by the service, since not all
|
||||
// watches have the same services encrypted (eg. #3308).
|
||||
huami2021ChunkedEncoder.write(chunk -> builder.write(characteristicChunked2021Write, chunk), endpoint, data, force2021Protocol(), mIsEncrypted.contains(endpoint));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToConfiguration(final TransactionBuilder builder, final byte[] data) {
|
||||
LOG.warn("writeToConfiguration is not supported");
|
||||
@@ -1015,7 +955,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
// In here, we only request the list of supported services - they will all be initialized in
|
||||
// initializeServices below
|
||||
mSupportedServices.clear();
|
||||
mIsEncrypted.clear();
|
||||
servicesService.requestServices(builder);
|
||||
}
|
||||
|
||||
@@ -1027,9 +966,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
|
||||
public void addSupportedService(final short endpoint, final boolean encrypted) {
|
||||
mSupportedServices.add(endpoint);
|
||||
if (encrypted) {
|
||||
mIsEncrypted.add(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
public void initializeServices() {
|
||||
@@ -1157,6 +1093,9 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
} else if (HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3_RECEIVE.equals(characteristicUUID)) {
|
||||
fileTransferService.onCharacteristicChanged(characteristic);
|
||||
return true;
|
||||
} else if (GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) {
|
||||
heartRateService.handleHeartRate(characteristic.getValue());
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onCharacteristicChanged(gatt, characteristic);
|
||||
@@ -1175,17 +1114,7 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Move these services to dedicated classes, so they get the encryption correctly
|
||||
switch (type) {
|
||||
case CHUNKED2021_ENDPOINT_HEARTRATE:
|
||||
handle2021HeartRate(payload);
|
||||
return;
|
||||
case CHUNKED2021_ENDPOINT_STEPS:
|
||||
handle2021Steps(payload);
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unhandled 2021 payload {}", String.format("0x%04x", type));
|
||||
}
|
||||
LOG.warn("Unhandled 2021 payload {}", String.format("0x%04x", type));
|
||||
}
|
||||
|
||||
public void setEncryptionParameters(final int encryptedSequenceNr, final byte[] sharedSessionKey) {
|
||||
@@ -1193,58 +1122,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
|
||||
huami2021ChunkedDecoder.setEncryptionParameters(sharedSessionKey);
|
||||
}
|
||||
|
||||
protected void handle2021HeartRate(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case HEART_RATE_CMD_REALTIME_ACK:
|
||||
// what does the status mean? Seems to be 0 on success
|
||||
LOG.info("Band acknowledged heart rate command, status = {}", payload[1]);
|
||||
return;
|
||||
case HEART_RATE_CMD_SLEEP:
|
||||
switch (payload[1]) {
|
||||
case HEART_RATE_FALL_ASLEEP:
|
||||
LOG.info("Fell asleep");
|
||||
processDeviceEvent(HuamiDeviceEvent.FELL_ASLEEP);
|
||||
break;
|
||||
case HEART_RATE_WAKE_UP:
|
||||
LOG.info("Woke up");
|
||||
processDeviceEvent(HuamiDeviceEvent.WOKE_UP);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unexpected sleep byte {}", String.format("0x%02x", payload[1]));
|
||||
break;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected heart rate byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
protected void handle2021Steps(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case STEPS_CMD_REPLY:
|
||||
LOG.info("Got steps reply, status = {}", payload[1]);
|
||||
if (payload.length != 15) {
|
||||
LOG.error("Unexpected steps reply payload length {}", payload.length);
|
||||
return;
|
||||
}
|
||||
handleRealtimeSteps(subarray(payload, 2, 15));
|
||||
return;
|
||||
case STEPS_CMD_ENABLE_REALTIME_ACK:
|
||||
LOG.info("Band acknowledged realtime steps, status = {}, enabled = {}", payload[1], payload[2]);
|
||||
return;
|
||||
case STEPS_CMD_REALTIME_NOTIFICATION:
|
||||
LOG.info("Got steps notification");
|
||||
if (payload.length != 14) {
|
||||
LOG.error("Unexpected realtime notification payload length {}", payload.length);
|
||||
return;
|
||||
}
|
||||
handleRealtimeSteps(subarray(payload, 1, 14));
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected steps payload byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileDownloadFinish(final String url, final String filename, final byte[] data) {
|
||||
LOG.info("File received: url={} filename={} length={}", url, filename, data.length);
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/* Copyright (C) 2025 José Rebelo
|
||||
|
||||
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.huami.zeppos.services;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepStateDetection;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.SleepState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.RealtimeSamplesAggregator;
|
||||
|
||||
public class ZeppOsHeartRateService extends AbstractZeppOsService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsHeartRateService.class);
|
||||
|
||||
private static final short ENDPOINT = 0x001d;
|
||||
|
||||
public static final byte CMD_REALTIME_SET = 0x04;
|
||||
public static final byte CMD_REALTIME_ACK = 0x05;
|
||||
public static final byte CMD_SLEEP = 0x06;
|
||||
|
||||
public static final byte SLEEP_EVENT_FALL_ASLEEP = 0x01;
|
||||
public static final byte SLEEP_EVENT_WAKE_UP = 0x00;
|
||||
|
||||
public static final byte REALTIME_MODE_STOP = 0x00;
|
||||
public static final byte REALTIME_MODE_START = 0x01;
|
||||
public static final byte REALTIME_MODE_CONTINUE = 0x02;
|
||||
|
||||
private final Handler realtimeHandler = new Handler();
|
||||
private SleepAsAndroidSender sleepAsAndroidSender;
|
||||
private RealtimeSamplesAggregator realtimeSamplesAggregator;
|
||||
|
||||
// Tracks whether realtime HR monitoring is already started, so we can just
|
||||
// send CONTINUE commands
|
||||
private boolean realtimeStarted = false;
|
||||
private boolean realtimeOneShot = false;
|
||||
|
||||
public ZeppOsHeartRateService(final ZeppOsSupport support) {
|
||||
super(support, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getEndpoint() {
|
||||
return ENDPOINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CMD_REALTIME_ACK:
|
||||
// what does the status mean? Seems to be 0 on success
|
||||
LOG.info("Band acknowledged heart rate command, status = {}", payload[1]);
|
||||
return;
|
||||
case CMD_SLEEP:
|
||||
switch (payload[1]) {
|
||||
case SLEEP_EVENT_FALL_ASLEEP:
|
||||
LOG.info("Fell asleep");
|
||||
evaluateGBDeviceEvent(new GBDeviceEventSleepStateDetection(SleepState.ASLEEP));
|
||||
break;
|
||||
case SLEEP_EVENT_WAKE_UP:
|
||||
LOG.info("Woke up");
|
||||
evaluateGBDeviceEvent(new GBDeviceEventSleepStateDetection(SleepState.AWAKE));
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unexpected sleep byte {}", String.format("0x%02x", payload[1]));
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.warn("Unexpected heart rate byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
realtimeHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
public void setSleepAsAndroidSender(final SleepAsAndroidSender sleepAsAndroidSender) {
|
||||
this.sleepAsAndroidSender = sleepAsAndroidSender;
|
||||
}
|
||||
|
||||
public void setRealtimeSamplesAggregator(final RealtimeSamplesAggregator realtimeSamplesAggregator) {
|
||||
this.realtimeSamplesAggregator = realtimeSamplesAggregator;
|
||||
}
|
||||
|
||||
public void onHeartRateTest() {
|
||||
LOG.debug("Trigger heart rate one-shot test");
|
||||
|
||||
realtimeStarted = true;
|
||||
realtimeOneShot = true;
|
||||
|
||||
final TransactionBuilder builder = new TransactionBuilder("HeartRateTest");
|
||||
builder.notify(getSupport().getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT), true);
|
||||
write(builder, new byte[]{CMD_REALTIME_SET, REALTIME_MODE_START});
|
||||
builder.queue(getSupport().getQueue());
|
||||
|
||||
realtimeHandler.removeCallbacksAndMessages(null);
|
||||
scheduleContinue();
|
||||
}
|
||||
|
||||
public void onEnableRealtimeHeartRateMeasurement(final boolean enable) {
|
||||
LOG.debug("Enable realtime hr: {}", enable);
|
||||
|
||||
if (enable == realtimeStarted) {
|
||||
// same state, ignore
|
||||
return;
|
||||
}
|
||||
|
||||
final byte hrCmd = enable ? REALTIME_MODE_START : REALTIME_MODE_STOP;
|
||||
|
||||
realtimeStarted = enable;
|
||||
realtimeOneShot = false;
|
||||
|
||||
final TransactionBuilder builder = new TransactionBuilder("set realtime heart rate measurement = " + enable);
|
||||
final BluetoothGattCharacteristic hrCharacteristic = getSupport().getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT);
|
||||
if (hrCharacteristic != null) {
|
||||
builder.notify(getSupport().getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT), enable);
|
||||
}
|
||||
write(builder, new byte[]{CMD_REALTIME_SET, hrCmd});
|
||||
builder.queue(getSupport().getQueue());
|
||||
|
||||
realtimeHandler.removeCallbacksAndMessages(null);
|
||||
if (enable) {
|
||||
scheduleContinue();
|
||||
}
|
||||
}
|
||||
|
||||
public void handleHeartRate(final byte[] value) {
|
||||
if (!realtimeOneShot && !realtimeStarted) {
|
||||
// Failsafe in case it gets out of sync, stop it
|
||||
onEnableRealtimeHeartRateMeasurement(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.length == 2 && value[0] == 0) {
|
||||
final int hrValue = (value[1] & 0xff);
|
||||
LOG.debug("Real-time hr: {}", hrValue);
|
||||
if (realtimeSamplesAggregator != null) {
|
||||
realtimeSamplesAggregator.broadcastHeartRate(hrValue);
|
||||
}
|
||||
if (sleepAsAndroidSender != null) {
|
||||
sleepAsAndroidSender.onHrChanged(hrValue, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (realtimeOneShot) {
|
||||
onEnableRealtimeHeartRateMeasurement(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleContinue() {
|
||||
realtimeHandler.postDelayed(() -> {
|
||||
sendContinue();
|
||||
scheduleContinue();
|
||||
}, 1000L);
|
||||
}
|
||||
|
||||
private void sendContinue() {
|
||||
final TransactionBuilder builder = new TransactionBuilder("hr continue");
|
||||
write(builder, new byte[]{CMD_REALTIME_SET, REALTIME_MODE_CONTINUE});
|
||||
builder.queue(getSupport().getQueue());
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/* Copyright (C) 2025 José Rebelo
|
||||
|
||||
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.huami.zeppos.services;
|
||||
|
||||
import static org.apache.commons.lang3.ArrayUtils.subarray;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.RealtimeSamplesAggregator;
|
||||
|
||||
public class ZeppOsStepsService extends AbstractZeppOsService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsStepsService.class);
|
||||
|
||||
private static final short ENDPOINT = 0x0016;
|
||||
|
||||
public static final byte CMD_GET = 0x03;
|
||||
public static final byte CMD_REPLY = 0x04;
|
||||
public static final byte CMD_ENABLE_REALTIME = 0x05;
|
||||
public static final byte CMD_ENABLE_REALTIME_ACK = 0x06;
|
||||
public static final byte CMD_REALTIME_NOTIFICATION = 0x07;
|
||||
|
||||
private RealtimeSamplesAggregator realtimeSamplesAggregator;
|
||||
|
||||
public ZeppOsStepsService(final ZeppOsSupport support) {
|
||||
super(support, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getEndpoint() {
|
||||
return ENDPOINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CMD_REPLY:
|
||||
LOG.info("Got steps reply, status = {}", payload[1]);
|
||||
if (payload.length != 15) {
|
||||
LOG.error("Unexpected steps reply payload length {}", payload.length);
|
||||
return;
|
||||
}
|
||||
handleRealtimeSteps(subarray(payload, 2, 15));
|
||||
return;
|
||||
case CMD_ENABLE_REALTIME_ACK:
|
||||
LOG.info("Band acknowledged realtime steps, status = {}, enabled = {}", payload[1], payload[2]);
|
||||
return;
|
||||
case CMD_REALTIME_NOTIFICATION:
|
||||
LOG.info("Got steps notification");
|
||||
if (payload.length != 14) {
|
||||
LOG.error("Unexpected realtime notification payload length {}", payload.length);
|
||||
return;
|
||||
}
|
||||
handleRealtimeSteps(subarray(payload, 1, 14));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.warn("Unexpected steps payload byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
|
||||
public void setRealtimeSamplesAggregator(final RealtimeSamplesAggregator realtimeSamplesAggregator) {
|
||||
this.realtimeSamplesAggregator = realtimeSamplesAggregator;
|
||||
}
|
||||
|
||||
public void onEnableRealtimeSteps(final boolean enable) {
|
||||
write("toggle realtime steps", new byte[]{CMD_ENABLE_REALTIME, bool(enable)});
|
||||
}
|
||||
|
||||
protected void handleRealtimeSteps(final byte[] value) {
|
||||
if (value.length != 13) {
|
||||
LOG.warn("Unrecognized realtime steps value: {}", Logging.formatBytes(value));
|
||||
return;
|
||||
}
|
||||
|
||||
final int steps = BLETypeConversions.toUint16(value, 1);
|
||||
LOG.debug("realtime steps: {}", steps);
|
||||
|
||||
if (realtimeSamplesAggregator != null) {
|
||||
realtimeSamplesAggregator.broadcastSteps(steps);
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-8
@@ -678,8 +678,7 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
LOG.debug("Current wearing state = {}, new wearing state = {}", currentWearingState, newState);
|
||||
|
||||
if (currentWearingState != WearingState.UNKNOWN && currentWearingState != newState) {
|
||||
GBDeviceEventWearState event = new GBDeviceEventWearState();
|
||||
event.wearingState = newState;
|
||||
GBDeviceEventWearState event = new GBDeviceEventWearState(newState);
|
||||
getSupport().evaluateGBDeviceEvent(event);
|
||||
}
|
||||
|
||||
@@ -704,8 +703,7 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
LOG.debug("Current sleep detection state = {}, new sleep detection state = {}", currentSleepDetectionState, newState);
|
||||
|
||||
if (currentSleepDetectionState != SleepState.UNKNOWN && currentSleepDetectionState != newState) {
|
||||
GBDeviceEventSleepStateDetection event = new GBDeviceEventSleepStateDetection();
|
||||
event.sleepState = newState;
|
||||
GBDeviceEventSleepStateDetection event = new GBDeviceEventSleepStateDetection(newState);
|
||||
getSupport().evaluateGBDeviceEvent(event);
|
||||
}
|
||||
|
||||
@@ -782,8 +780,7 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
|
||||
// send event if the previous state is known and the new state is different from cached
|
||||
if (currentSleepDetectionState != SleepState.UNKNOWN && currentSleepDetectionState != newSleepState) {
|
||||
GBDeviceEventSleepStateDetection event = new GBDeviceEventSleepStateDetection();
|
||||
event.sleepState = newSleepState;
|
||||
GBDeviceEventSleepStateDetection event = new GBDeviceEventSleepStateDetection(newSleepState);
|
||||
getSupport().evaluateGBDeviceEvent(event);
|
||||
}
|
||||
|
||||
@@ -796,8 +793,7 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
LOG.debug("Previous wearing state: {}, new wearing state: {}", currentWearingState, newWearingState);
|
||||
|
||||
if (currentWearingState != WearingState.UNKNOWN && currentWearingState != newWearingState) {
|
||||
GBDeviceEventWearState event = new GBDeviceEventWearState();
|
||||
event.wearingState = newWearingState;
|
||||
GBDeviceEventWearState event = new GBDeviceEventWearState(newWearingState);
|
||||
getSupport().evaluateGBDeviceEvent(event);
|
||||
}
|
||||
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/* Copyright (C) 2025 José Rebelo
|
||||
|
||||
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.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
|
||||
/**
|
||||
* A simple sample aggregator that takes values from multiple sources for steps and HR, and broadcasts
|
||||
* the resulting activity sample. Invalidates previously seen values after a certain timeout.
|
||||
*/
|
||||
public class RealtimeSamplesAggregator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RealtimeSamplesAggregator.class);
|
||||
|
||||
private final Context context;
|
||||
private final GBDevice gbDevice;
|
||||
|
||||
private long lastHeartRateTime = -1;
|
||||
private int heartRate = -1;
|
||||
|
||||
private long lastStepsTime = -1;
|
||||
private int previousSteps = -1;
|
||||
|
||||
public RealtimeSamplesAggregator(final Context context, final GBDevice gbDevice) {
|
||||
this.context = context;
|
||||
this.gbDevice = gbDevice;
|
||||
}
|
||||
|
||||
public void broadcastSteps(final int steps) {
|
||||
if (System.currentTimeMillis() - lastStepsTime > 60_000L) {
|
||||
previousSteps = -1;
|
||||
}
|
||||
|
||||
lastStepsTime = System.currentTimeMillis();
|
||||
|
||||
if (previousSteps == -1) {
|
||||
previousSteps = steps;
|
||||
}
|
||||
|
||||
broadcast(heartRate, steps - previousSteps);
|
||||
}
|
||||
|
||||
public void broadcastHeartRate(final int newHeartRate) {
|
||||
lastHeartRateTime = System.currentTimeMillis();
|
||||
heartRate = newHeartRate;
|
||||
|
||||
broadcast(heartRate, 0);
|
||||
}
|
||||
|
||||
private void broadcast(final int hr, final int steps) {
|
||||
final AbstractActivitySample sample;
|
||||
try (final DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = dbHandler.getDaoSession();
|
||||
final Device device = DBHelper.getDevice(gbDevice, session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
|
||||
final SampleProvider<? extends ActivitySample> sampleProvider = gbDevice.getDeviceCoordinator().getSampleProvider(gbDevice, session);
|
||||
sample = sampleProvider.createActivitySample();
|
||||
if (!(sample instanceof Serializable)) {
|
||||
LOG.error("Activity sample {} is not Serializable!", sample.getClass());
|
||||
return;
|
||||
}
|
||||
|
||||
sample.setProvider(sampleProvider);
|
||||
sample.setDeviceId(device.getId());
|
||||
sample.setUserId(user.getId());
|
||||
sample.setTimestamp((int) (System.currentTimeMillis() / 1000));
|
||||
|
||||
if (System.currentTimeMillis() - lastHeartRateTime < 10_000L) {
|
||||
sample.setHeartRate(hr);
|
||||
} else {
|
||||
sample.setSteps(-1);
|
||||
}
|
||||
if (System.currentTimeMillis() - lastStepsTime < 60_000L) {
|
||||
sample.setSteps(steps);
|
||||
} else {
|
||||
sample.setSteps(0);
|
||||
}
|
||||
sample.setRawKind(sampleProvider.toRawActivityKind(ActivityKind.UNKNOWN));
|
||||
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Error creating activity sample", e);
|
||||
return;
|
||||
}
|
||||
|
||||
final Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
|
||||
.putExtra(GBDevice.EXTRA_DEVICE, gbDevice)
|
||||
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, (Serializable) sample);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user