/* Copyright (C) 2024 Arjan Schrijver
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 . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.colmi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.ColmiR0xConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.ColmiR0xPacketHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiHeartRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
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.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
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.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(ColmiR0xDeviceSupport.class);
private final Handler backgroundTasksHandler = new Handler(Looper.getMainLooper());
private final DeviceInfoProfile deviceInfoProfile;
private String cachedFirmwareVersion = null;
private int daysAgo;
private int packetsTotalNr;
private Calendar syncingDay;
private int bigDataPacketSize;
private ByteBuffer bigDataPacket;
public ColmiR0xDeviceSupport() {
super(LOG);
addSupportedService(ColmiR0xConstants.CHARACTERISTIC_SERVICE_V1);
addSupportedService(ColmiR0xConstants.CHARACTERISTIC_SERVICE_V2);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
IntentListener mListener = intent -> {
String action = intent.getAction();
if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) {
handleDeviceInfo(intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
}
};
deviceInfoProfile = new DeviceInfoProfile<>(this);
deviceInfoProfile.addListener(mListener);
addSupportedProfile(deviceInfoProfile);
}
@Override
public void dispose() {
backgroundTasksHandler.removeCallbacksAndMessages(null);
super.dispose();
}
@Override
public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) {
if (gbDevice.getFirmwareVersion() != null) {
setCachedFirmwareVersion(gbDevice.getFirmwareVersion());
}
super.setContext(gbDevice, btAdapter, context);
}
public String getCachedFirmwareVersion() {
return this.cachedFirmwareVersion;
}
public void setCachedFirmwareVersion(String version) {
this.cachedFirmwareVersion = version;
}
private void handleDeviceInfo(DeviceInfo info) {
LOG.debug("Device info: {}", info);
GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
versionCmd.hwVersion = info.getHardwareRevision();
versionCmd.fwVersion = info.getFirmwareRevision();
handleGBDeviceEvent(versionCmd);
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
if (getDevice().getFirmwareVersion() == null) {
getDevice().setFirmwareVersion(getCachedFirmwareVersion() != null ? getCachedFirmwareVersion() : "N/A");
}
deviceInfoProfile.requestDeviceInfo(builder);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
builder.notify(getCharacteristic(ColmiR0xConstants.CHARACTERISTIC_NOTIFY_V1), true);
builder.notify(getCharacteristic(ColmiR0xConstants.CHARACTERISTIC_NOTIFY_V2), true);
// Delay initialization with 2 seconds to give the ring time to settle
backgroundTasksHandler.removeCallbacksAndMessages(null);
backgroundTasksHandler.postDelayed(this::postConnectInitialization, 2000);
return builder;
}
private void postConnectInitialization() {
setPhoneName();
setDateTime();
setUserPreferences();
requestBatteryInfo();
requestSettingsFromRing();
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
if (super.onCharacteristicChanged(gatt, characteristic)) {
return true;
}
UUID characteristicUUID = characteristic.getUuid();
byte[] value = characteristic.getValue();
LOG.debug("Characteristic {} changed, value: {}", characteristicUUID, StringUtils.bytesToHex(characteristic.getValue()));
if (characteristicUUID.equals(ColmiR0xConstants.CHARACTERISTIC_NOTIFY_V1)) {
switch (value[0]) {
case ColmiR0xConstants.CMD_SET_DATE_TIME:
LOG.info("Received set date/time response: {}", StringUtils.bytesToHex(value));
break;
case ColmiR0xConstants.CMD_BATTERY:
int levelResponse = value[1];
boolean charging = value[2] == 1;
LOG.info("Received battery level response: {}% (charging: {})", levelResponse, charging);
GBDeviceEventBatteryInfo batteryEvent = createDeviceBatteryInfoEvent(levelResponse, charging);
evaluateGBDeviceEvent(batteryEvent);
break;
case ColmiR0xConstants.CMD_PHONE_NAME:
LOG.info("Received phone name response: {}", StringUtils.bytesToHex(value));
break;
case ColmiR0xConstants.CMD_PREFERENCES:
LOG.info("Received user preferences response: {}", StringUtils.bytesToHex(value));
break;
case ColmiR0xConstants.CMD_SYNC_HEART_RATE:
LOG.info("Received HR history sync packet: {}", StringUtils.bytesToHex(value));
int hrPacketNr = value[1] & 0xff;
if (hrPacketNr == 0xff) {
LOG.info("Empty HR history, sync aborted");
getDevice().unsetBusyTask();
getDevice().sendDeviceUpdateIntent(getContext());
} else if (hrPacketNr == 0) {
packetsTotalNr = value[2];
LOG.info("HR history packet {} out of total {}", hrPacketNr, packetsTotalNr);
} else {
LOG.info("HR history packet {} out of total {} (data for {}:00-{}:00)", hrPacketNr, packetsTotalNr, hrPacketNr-1, hrPacketNr);
Calendar sampleCal = (Calendar) syncingDay.clone();
int startValue = hrPacketNr == 1 ? 6 : 2; // packet 1 contains the sync-from timestamp in bytes 2-5
int minutesInPreviousPackets = 0;
if (hrPacketNr == 1) {
int timestamp = BLETypeConversions.toUint32(value[2], value[3], value[4], value[5]);
Date timestampDate = DateTimeUtils.parseTimeStamp(timestamp);
LOG.info("Receiving HR history sequence with timestamp {}", DateTimeUtils.formatIso8601UTC(timestampDate));
} else {
minutesInPreviousPackets = 9 * 5; // packet 1
minutesInPreviousPackets += (hrPacketNr - 2) * 13 * 5;
}
for (int i = startValue; i < value.length - 1; i++) {
if (value[i] != 0x00) {
// Determine time of day
int minuteOfDay = minutesInPreviousPackets + (i - startValue) * 5;
sampleCal.set(Calendar.HOUR_OF_DAY, minuteOfDay / 60);
sampleCal.set(Calendar.MINUTE, minuteOfDay % 60);
sampleCal.set(Calendar.SECOND, 0);
LOG.info("Value {} is {} bpm, time of day is {}", i, value[i] & 0xff, sampleCal.getTime());
// Build sample object and save in database
try (DBHandler db = GBApplication.acquireDB()) {
ColmiHeartRateSampleProvider sampleProvider = new ColmiHeartRateSampleProvider(getDevice(), db.getDaoSession());
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), db.getDaoSession()).getId();
ColmiHeartRateSample gbSample = new ColmiHeartRateSample();
gbSample.setDeviceId(deviceId);
gbSample.setUserId(userId);
gbSample.setTimestamp(sampleCal.getTimeInMillis());
gbSample.setHeartRate(value[i] & 0xff);
sampleProvider.addSample(gbSample);
} catch (Exception e) {
LOG.error("Error acquiring database for recording heart rate samples", e);
}
}
}
if (hrPacketNr == packetsTotalNr - 1) {
getDevice().unsetBusyTask();
getDevice().sendDeviceUpdateIntent(getContext());
}
}
if (!getDevice().isBusy()) {
if (daysAgo < 7) {
daysAgo++;
fetchHistoryHR();
} else {
fetchHistoryStress();
}
}
break;
case ColmiR0xConstants.CMD_AUTO_HR_PREF:
ColmiR0xPacketHandler.hrIntervalSettings(this, value);
break;
case ColmiR0xConstants.CMD_GOALS:
ColmiR0xPacketHandler.goalsSettings(value);
break;
case ColmiR0xConstants.CMD_AUTO_SPO2_PREF:
ColmiR0xPacketHandler.spo2Settings(this, value);
break;
case ColmiR0xConstants.CMD_PACKET_SIZE:
LOG.info("Received packet size indicator: {} bytes", value[1] & 0xff);
break;
case ColmiR0xConstants.CMD_AUTO_STRESS_PREF:
ColmiR0xPacketHandler.stressSettings(this, value);
break;
case ColmiR0xConstants.CMD_AUTO_HRV_PREF:
ColmiR0xPacketHandler.hrvSettings(this, value);
break;
case ColmiR0xConstants.CMD_SYNC_STRESS:
ColmiR0xPacketHandler.historicalStress(getDevice(), getContext(), value);
if (!getDevice().isBusy()) {
fetchHistorySpo2();
}
break;
case ColmiR0xConstants.CMD_SYNC_ACTIVITY:
ColmiR0xPacketHandler.historicalActivity(getDevice(), getContext(), value);
if (!getDevice().isBusy()) {
if (daysAgo < 7) {
daysAgo++;
fetchHistoryActivity();
} else {
daysAgo = 0;
fetchHistoryHR();
}
}
break;
case ColmiR0xConstants.CMD_SYNC_HRV:
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_hrv_data));
ColmiR0xPacketHandler.historicalHRV(getDevice(), getContext(), value, daysAgo);
if (!getDevice().isBusy()) {
if (daysAgo < 6) {
daysAgo++;
fetchHistoryHRV();
} else {
fetchRecordedDataFinished();
}
}
break;
case ColmiR0xConstants.CMD_FIND_DEVICE:
LOG.info("Received find device response: {}", StringUtils.bytesToHex(value));
break;
case ColmiR0xConstants.CMD_MANUAL_HEART_RATE:
ColmiR0xPacketHandler.liveHeartRate(getDevice(), getContext(), value);
break;
case ColmiR0xConstants.CMD_NOTIFICATION:
switch (value[1]) {
case ColmiR0xConstants.NOTIFICATION_NEW_HR_DATA:
LOG.info("Received notification from ring that new HR data is available to sync");
break;
case ColmiR0xConstants.NOTIFICATION_NEW_SPO2_DATA:
LOG.info("Received notification from ring that new SpO2 data is available to sync");
break;
case ColmiR0xConstants.NOTIFICATION_NEW_STEPS_DATA:
LOG.info("Received notification from ring that new steps data is available to sync");
break;
case ColmiR0xConstants.NOTIFICATION_BATTERY_LEVEL:
int levelNotif = value[2];
charging = value[3] == 1;
LOG.info("Received battery level notification: {}% (charging: {})", levelNotif, charging);
GBDeviceEventBatteryInfo batteryNotifEvent = createDeviceBatteryInfoEvent(levelNotif, charging);
evaluateGBDeviceEvent(batteryNotifEvent);
break;
case ColmiR0xConstants.NOTIFICATION_LIVE_ACTIVITY:
ColmiR0xPacketHandler.liveActivity(value);
break;
default:
LOG.info("Received unrecognized notification: {}", StringUtils.bytesToHex(value));
break;
}
break;
default:
LOG.info("Received unrecognized packet: {}", StringUtils.bytesToHex(value));
break;
}
}
if (characteristicUUID.equals(ColmiR0xConstants.CHARACTERISTIC_NOTIFY_V2)) {
// Big data responses can arrive in multiple packets that need to be concatenated
if (bigDataPacket != null) {
LOG.debug("Received {} bytes on big data characteristic while waiting for follow-up data", value.length);
bigDataPacket.rewind();
ByteBuffer concatenated = ByteBuffer
.allocate(bigDataPacket.limit() + value.length)
.put(bigDataPacket)
.put(value);
bigDataPacket = concatenated;
if (bigDataPacket.limit() < bigDataPacketSize + 6) {
// If the received data is smaller than the expected packet size (+ 6 bytes header),
// wait for the next packet and append it
LOG.debug("Big data packet is not complete yet, got {} bytes while expecting {}+6. Waiting for more...", bigDataPacket.limit(), bigDataPacketSize);
return true;
} else {
value = bigDataPacket.array();
bigDataPacket = null;
LOG.debug("Big data packet complete, got {} bytes while expecting {}+6", value.length, bigDataPacketSize);
}
}
switch (value[0]) {
case ColmiR0xConstants.CMD_BIG_DATA_V2:
int packetLength = BLETypeConversions.toUint16(value[2], value[3]);
if (value.length < packetLength + 6) {
// If the received packet is smaller than the expected packet size (+ 6 bytes header),
// wait for the next packet and append it
LOG.debug("Big data packet is not complete yet, got {} bytes while expecting {}+6. Waiting for more...", value.length, packetLength);
bigDataPacketSize = packetLength;
bigDataPacket = ByteBuffer.wrap(value);
return true;
}
switch (value[1]) {
case ColmiR0xConstants.BIG_DATA_TYPE_SLEEP:
ColmiR0xPacketHandler.historicalSleep(getDevice(), getContext(), value);
daysAgo = 0;
fetchHistoryHRV();
// Signal history sync finished at this point, since older firmwares
// will not send anything back after requesting HRV history
fetchRecordedDataFinished();
break;
case ColmiR0xConstants.BIG_DATA_TYPE_SPO2:
ColmiR0xPacketHandler.historicalSpo2(getDevice(), value);
fetchHistorySleep();
break;
default:
LOG.info("Received unrecognized big data packet: {}", StringUtils.bytesToHex(value));
break;
}
break;
default:
LOG.info("Received unrecognized notify v2 packet: {}", StringUtils.bytesToHex(value));
break;
}
return true;
}
return false;
}
@NonNull
private static GBDeviceEventBatteryInfo createDeviceBatteryInfoEvent(int levelResponse, boolean charging) {
GBDeviceEventBatteryInfo batteryEvent = new GBDeviceEventBatteryInfo();
batteryEvent.level = levelResponse;
batteryEvent.state = charging ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
return batteryEvent;
}
private byte[] buildPacket(byte[] contents) {
ByteBuffer buffer = ByteBuffer.allocate(16);
if (contents.length <= 15) {
buffer.put(contents);
int checksum = 0;
for (byte content : contents) {
checksum = (byte) (checksum + content) & 0xff;
}
buffer.put(15, (byte) checksum);
} else {
LOG.warn("Packet content too long!");
}
return buffer.array();
}
private void sendWrite(String taskName, byte[] contents) {
TransactionBuilder builder = new TransactionBuilder(taskName);
BluetoothGattCharacteristic characteristic = getCharacteristic(ColmiR0xConstants.CHARACTERISTIC_WRITE);
if (characteristic != null) {
builder.write(characteristic, contents);
builder.queue(getQueue());
}
}
private void sendCommand(String taskName, byte[] contents) {
TransactionBuilder builder = new TransactionBuilder(taskName);
BluetoothGattCharacteristic characteristic = getCharacteristic(ColmiR0xConstants.CHARACTERISTIC_COMMAND);
if (characteristic != null) {
builder.write(characteristic, contents);
builder.queue(getQueue());
}
}
private void requestBatteryInfo() {
byte[] batteryRequestPacket = buildPacket(new byte[]{ColmiR0xConstants.CMD_BATTERY});
LOG.info("Battery request sent: {}", StringUtils.bytesToHex(batteryRequestPacket));
sendWrite("batteryRequest", batteryRequestPacket);
}
private void setPhoneName() {
byte[] setPhoneNamePacket = buildPacket(new byte[]{
ColmiR0xConstants.CMD_PHONE_NAME,
0x02, // Client major version
0x0a, // Client minor version
'G',
'B'
});
LOG.info("Phone name sent: {}", StringUtils.bytesToHex(setPhoneNamePacket));
sendWrite("phoneNameRequest", setPhoneNamePacket);
}
private void setDateTime() {
Calendar now = GregorianCalendar.getInstance();
byte[] setDateTimePacket = buildPacket(new byte[]{
ColmiR0xConstants.CMD_SET_DATE_TIME,
Byte.parseByte(String.valueOf(now.get(Calendar.YEAR) % 2000), 16),
Byte.parseByte(String.valueOf(now.get(Calendar.MONTH) + 1), 16),
Byte.parseByte(String.valueOf(now.get(Calendar.DAY_OF_MONTH)), 16),
Byte.parseByte(String.valueOf(now.get(Calendar.HOUR_OF_DAY)), 16),
Byte.parseByte(String.valueOf(now.get(Calendar.MINUTE)), 16),
Byte.parseByte(String.valueOf(now.get(Calendar.SECOND)), 16)
});
LOG.info("Set date/time request sent: {}", StringUtils.bytesToHex(setDateTimePacket));
sendWrite("dateTimeRequest", setDateTimePacket);
}
@Override
public void onSetTime() {
setDateTime();
}
@Override
public void onSendConfiguration(String config) {
final Prefs prefs = getDevicePrefs();
switch (config) {
case SettingsActivity.PREF_MEASUREMENT_SYSTEM:
setUserPreferences();
break;
case DeviceSettingsPreferenceConst.PREF_SPO2_ALL_DAY_MONITORING:
final boolean spo2Enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SPO2_ALL_DAY_MONITORING, false);
byte[] spo2PrefsPacket = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_SPO2_PREF, ColmiR0xConstants.PREF_WRITE, (byte) (spo2Enabled ? 0x01 : 0x00)});
LOG.info("SpO2 preference request sent: {}", StringUtils.bytesToHex(spo2PrefsPacket));
sendWrite("spo2PreferenceRequest", spo2PrefsPacket);
break;
case DeviceSettingsPreferenceConst.PREF_HEARTRATE_STRESS_MONITORING:
final boolean stressEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HEARTRATE_STRESS_MONITORING, false);
byte[] stressPrefsPacket = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_STRESS_PREF, ColmiR0xConstants.PREF_WRITE, (byte) (stressEnabled ? 0x01 : 0x00)});
LOG.info("Stress preference request sent: {}", StringUtils.bytesToHex(stressPrefsPacket));
sendWrite("stressPreferenceRequest", stressPrefsPacket);
break;
case DeviceSettingsPreferenceConst.PREF_HRV_ALL_DAY_MONITORING:
final boolean hrvEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HRV_ALL_DAY_MONITORING, false);
byte[] hrvPrefsPacket = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_HRV_PREF, ColmiR0xConstants.PREF_WRITE, (byte) (hrvEnabled ? 0x01 : 0x00)});
LOG.info("HRV preference request sent: {}", StringUtils.bytesToHex(hrvPrefsPacket));
sendWrite("hrvPreferenceRequest", hrvPrefsPacket);
break;
}
}
@Override
public void onSetHeartRateMeasurementInterval(int seconds) {
// Round to nearest 5 minutes and limit to 60 minutes due to device constraints
long hrIntervalMins = Math.min(Math.round(seconds / 60.0 / 5.0) * 5, 60);
byte[] hrIntervalPacket = buildPacket(new byte[]{
ColmiR0xConstants.CMD_AUTO_HR_PREF,
ColmiR0xConstants.PREF_WRITE,
hrIntervalMins > 0 ? (byte) 0x01 : (byte) 0x02,
(byte) hrIntervalMins
});
LOG.info("HR interval preference request sent: {}", StringUtils.bytesToHex(hrIntervalPacket));
sendWrite("hrIntervalPreferenceRequest", hrIntervalPacket);
}
private void setUserPreferences() {
final Prefs prefs = getDevicePrefs();
final ActivityUser user = new ActivityUser();
final String measurementSystem = prefs.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, "metric");
byte userGender;
switch (user.getGender()) {
case ActivityUser.GENDER_FEMALE:
userGender = 0x01;
break;
case ActivityUser.GENDER_MALE:
userGender = 0x00;
break;
default:
userGender = 0x02;
break;
}
byte[] userPrefsPacket = buildPacket(new byte[]{
ColmiR0xConstants.CMD_PREFERENCES,
ColmiR0xConstants.PREF_WRITE,
0x00, // 24h format, 0x01 is 12h format
(byte) ("metric".equals(measurementSystem) ? 0x00 : 0x01),
userGender,
(byte) user.getAge(),
(byte) user.getHeightCm(),
(byte) user.getWeightKg(),
0x00, // systolic blood pressure (e.g. 120)
0x00, // diastolic blood pressure (e.g. 90)
0x00 // heart rate value warning threshold: (e.g. 160)
});
LOG.info("User preferences request sent: {}", StringUtils.bytesToHex(userPrefsPacket));
sendWrite("userPreferenceRequest", userPrefsPacket);
}
private void requestSettingsFromRing() {
byte[] request = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_HR_PREF, ColmiR0xConstants.PREF_READ});
LOG.info("Request HR measurement interval from ring: {}", StringUtils.bytesToHex(request));
sendWrite("hrIntervalRequest", request);
request = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_STRESS_PREF, ColmiR0xConstants.PREF_READ});
LOG.info("Request stress measurement setting from ring: {}", StringUtils.bytesToHex(request));
sendWrite("stressSettingRequest", request);
request = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_SPO2_PREF, ColmiR0xConstants.PREF_READ});
LOG.info("Request SpO2 measurement setting from ring: {}", StringUtils.bytesToHex(request));
sendWrite("spo2SettingRequest", request);
request = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_HRV_PREF, ColmiR0xConstants.PREF_READ});
LOG.info("Request HRV measurement setting from ring: {}", StringUtils.bytesToHex(request));
sendWrite("hrvSettingRequest", request);
request = buildPacket(new byte[]{ColmiR0xConstants.CMD_GOALS, ColmiR0xConstants.PREF_READ});
LOG.info("Request goals from ring: {}", StringUtils.bytesToHex(request));
sendWrite("goalsSettingRequest", request);
}
@Override
public void onPowerOff() {
byte[] poweroffPacket = buildPacket(new byte[]{ColmiR0xConstants.CMD_POWER_OFF, 0x01});
LOG.info("Poweroff request sent: {}", StringUtils.bytesToHex(poweroffPacket));
sendWrite("poweroffRequest", poweroffPacket);
}
@Override
public void onReset(int flags) {
if ((flags & GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET) != 0) {
byte[] resetPacket = buildPacket(new byte[]{ColmiR0xConstants.CMD_FACTORY_RESET, 0x66, 0x66});
LOG.info("Factory reset request sent: {}", StringUtils.bytesToHex(resetPacket));
sendWrite("resetRequest", resetPacket);
}
}
@Override
public void onFindDevice(boolean start) {
if (!start) return;
byte[] findDevicePacket = buildPacket(new byte[]{ColmiR0xConstants.CMD_FIND_DEVICE, 0x55, (byte) 0xAA});
LOG.info("Find device request sent: {}", StringUtils.bytesToHex(findDevicePacket));
sendWrite("findDeviceRequest", findDevicePacket);
}
@Override
public void onHeartRateTest() {
byte[] measureHeartRatePacket = buildPacket(new byte[]{ColmiR0xConstants.CMD_MANUAL_HEART_RATE, 0x01});
LOG.info("Measure HR request sent: {}", StringUtils.bytesToHex(measureHeartRatePacket));
sendWrite("measureHRRequest", measureHeartRatePacket);
}
@Override
public void onFetchRecordedData(int dataTypes) {
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data), "", true, 0, getContext());
daysAgo = 0;
fetchHistoryActivity();
}
private void fetchRecordedDataFinished() {
GB.updateTransferNotification(null, "", false, 100, getContext());
LOG.info("Sync finished!");
getDevice().unsetBusyTask();
getDevice().sendDeviceUpdateIntent(getContext());
GB.signalActivityDataFinish(getDevice());
}
private void fetchHistoryActivity() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data));
getDevice().sendDeviceUpdateIntent(getContext());
syncingDay = Calendar.getInstance();
syncingDay.add(Calendar.DAY_OF_MONTH, 0 - daysAgo);
syncingDay.set(Calendar.HOUR_OF_DAY, 0);
syncingDay.set(Calendar.MINUTE, 0);
syncingDay.set(Calendar.SECOND, 0);
syncingDay.set(Calendar.MILLISECOND, 0);
byte[] activityHistoryRequest = buildPacket(new byte[]{ColmiR0xConstants.CMD_SYNC_ACTIVITY, (byte) daysAgo, 0x0f, 0x00, 0x5f, 0x01});
LOG.info("Fetch historical activity data request sent: {}", StringUtils.bytesToHex(activityHistoryRequest));
sendWrite("activityHistoryRequest", activityHistoryRequest);
}
private void fetchHistoryHR() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_hr_data));
getDevice().sendDeviceUpdateIntent(getContext());
syncingDay = Calendar.getInstance();
if (daysAgo != 0) {
syncingDay.add(Calendar.DAY_OF_MONTH, 0 - daysAgo);
syncingDay.set(Calendar.HOUR_OF_DAY, 0);
syncingDay.set(Calendar.MINUTE, 0);
syncingDay.set(Calendar.SECOND, 0);
}
syncingDay.set(Calendar.MILLISECOND, 0);
ByteBuffer hrHistoryRequestBB = ByteBuffer.allocate(5);
hrHistoryRequestBB.order(ByteOrder.LITTLE_ENDIAN);
hrHistoryRequestBB.put(0, ColmiR0xConstants.CMD_SYNC_HEART_RATE);
long requestTimestamp = syncingDay.getTimeInMillis() + syncingDay.get(Calendar.ZONE_OFFSET) + syncingDay.get(Calendar.DST_OFFSET);
hrHistoryRequestBB.putInt(1, (int) (requestTimestamp / 1000));
byte[] hrHistoryRequest = buildPacket(hrHistoryRequestBB.array());
LOG.info("Fetch historical HR data request sent ({}): {}", DateTimeUtils.formatIso8601(syncingDay.getTime()), StringUtils.bytesToHex(hrHistoryRequest));
sendWrite("hrHistoryRequest", hrHistoryRequest);
}
private void fetchHistoryStress() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_stress_data));
getDevice().sendDeviceUpdateIntent(getContext());
syncingDay = Calendar.getInstance();
byte[] stressHistoryRequest = buildPacket(new byte[]{ColmiR0xConstants.CMD_SYNC_STRESS});
LOG.info("Fetch historical stress data request sent: {}", StringUtils.bytesToHex(stressHistoryRequest));
sendWrite("stressHistoryRequest", stressHistoryRequest);
}
private void fetchHistorySpo2() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_spo2_data));
getDevice().sendDeviceUpdateIntent(getContext());
byte[] spo2HistoryRequest = new byte[]{
ColmiR0xConstants.CMD_BIG_DATA_V2,
ColmiR0xConstants.BIG_DATA_TYPE_SPO2,
0x01,
0x00,
(byte) 0xff,
0x00,
(byte) 0xff
};
LOG.info("Fetch historical SpO2 data request sent: {}", StringUtils.bytesToHex(spo2HistoryRequest));
sendCommand("spo2HistoryRequest", spo2HistoryRequest);
}
private void fetchHistorySleep() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_sleep_data));
getDevice().sendDeviceUpdateIntent(getContext());
byte[] sleepHistoryRequest = new byte[]{
ColmiR0xConstants.CMD_BIG_DATA_V2,
ColmiR0xConstants.BIG_DATA_TYPE_SLEEP,
0x01,
0x00,
(byte) 0xff,
0x00,
(byte) 0xff
};
LOG.info("Fetch historical sleep data request sent: {}", StringUtils.bytesToHex(sleepHistoryRequest));
sendCommand("sleepHistoryRequest", sleepHistoryRequest);
}
private void fetchHistoryHRV() {
getDevice().sendDeviceUpdateIntent(getContext());
syncingDay = Calendar.getInstance();
if (daysAgo != 0) {
syncingDay.add(Calendar.DAY_OF_MONTH, 0 - daysAgo);
syncingDay.set(Calendar.HOUR_OF_DAY, 0);
syncingDay.set(Calendar.MINUTE, 0);
}
syncingDay.set(Calendar.SECOND, 0);
syncingDay.set(Calendar.MILLISECOND, 0);
ByteBuffer hrvHistoryRequestBB = ByteBuffer.allocate(5);
hrvHistoryRequestBB.order(ByteOrder.LITTLE_ENDIAN);
hrvHistoryRequestBB.put(0, ColmiR0xConstants.CMD_SYNC_HRV);
hrvHistoryRequestBB.putInt(1, daysAgo);
byte[] hrvHistoryRequest = buildPacket(hrvHistoryRequestBB.array());
LOG.info("Fetch historical HRV data request sent ({}): {}", syncingDay.getTime(), StringUtils.bytesToHex(hrvHistoryRequest));
sendWrite("hrvHistoryRequest", hrvHistoryRequest);
}
}