Zepp OS: Extract services: user info, silent mode, find device

This commit is contained in:
José Rebelo
2025-04-03 21:23:56 +01:00
parent 55c9f689df
commit 1739c9c585
10 changed files with 464 additions and 284 deletions
@@ -22,30 +22,14 @@ public class Huami2021Service {
*/
public static final short CHUNKED2021_ENDPOINT_WEATHER = 0x000e;
public static final short CHUNKED2021_ENDPOINT_CONNECTION = 0x0015;
public static final short CHUNKED2021_ENDPOINT_USER_INFO = 0x0017;
public static final short CHUNKED2021_ENDPOINT_STEPS = 0x0016;
public static final short CHUNKED2021_ENDPOINT_VIBRATION_PATTERNS = 0x0018;
public static final short CHUNKED2021_ENDPOINT_WORKOUT = 0x0019;
public static final short CHUNKED2021_ENDPOINT_FIND_DEVICE = 0x001a;
public static final short CHUNKED2021_ENDPOINT_HEARTRATE = 0x001d;
public static final short CHUNKED2021_ENDPOINT_BATTERY = 0x0029;
public static final short CHUNKED2021_ENDPOINT_SILENT_MODE = 0x003b;
public static final short CHUNKED2021_ENDPOINT_AUTH = 0x0082;
public static final short CHUNKED2021_ENDPOINT_COMPAT = 0x0090;
/**
* Find Device, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_FIND_DEVICE}.
*/
public static final byte FIND_BAND_START = 0x03;
public static final byte FIND_BAND_ACK = 0x04;
public static final byte FIND_BAND_STOP_FROM_PHONE = 0x06;
public static final byte FIND_BAND_STOP_FROM_BAND = 0x07;
public static final byte FIND_PHONE_START = 0x11;
public static final byte FIND_PHONE_ACK = 0x12;
public static final byte FIND_PHONE_STOP_FROM_BAND = 0x13;
public static final byte FIND_PHONE_STOP_FROM_PHONE = 0x14;
public static final byte FIND_PHONE_MODE = 0x15;
/**
* Steps, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_STEPS}.
*/
@@ -67,22 +51,6 @@ public class Huami2021Service {
public static final byte BATTERY_REQUEST = 0x03;
public static final byte BATTERY_REPLY = 0x04;
/**
* Silent Mode, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_SILENT_MODE}.
*/
public static final byte SILENT_MODE_CMD_CAPABILITIES_REQUEST = 0x01;
public static final byte SILENT_MODE_CMD_CAPABILITIES_RESPONSE = 0x02;
// Notify silent mode, from phone
public static final byte SILENT_MODE_CMD_NOTIFY_BAND = 0x03;
public static final byte SILENT_MODE_CMD_NOTIFY_BAND_ACK = 0x04;
// Query silent mode on phone, from band
public static final byte SILENT_MODE_CMD_QUERY = 0x05;
public static final byte SILENT_MODE_CMD_REPLY = 0x06;
// Set silent mode on phone, from band
// After this, phone sends ACK + NOTIFY
public static final byte SILENT_MODE_CMD_SET = 0x07;
public static final byte SILENT_MODE_CMD_ACK = 0x08;
/**
* Connection, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_CONNECTION}.
*/
@@ -120,12 +88,6 @@ public class Huami2021Service {
public static final byte WEATHER_CMD_SET_DEFAULT_LOCATION = 0x09;
public static final byte WEATHER_CMD_DEFAULT_LOCATION_ACK = 0x0a;
/**
* User Info, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_USER_INFO}.
*/
public static final byte USER_INFO_CMD_SET = 0x01;
public static final byte USER_INFO_CMD_SET_ACK = 0x02;
/**
* Raw sensor control.
*/
@@ -95,7 +95,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.zeppos.ZeppOsCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
@@ -1649,29 +1648,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
}
protected int getFindDeviceInterval() {
final VibrationProfile findBand = HuamiCoordinator.getVibrationProfile(
getDevice().getAddress(),
HuamiVibrationPatternNotificationType.FIND_BAND,
supportsDeviceDefaultVibrationProfiles()
);
int findDeviceInterval = 0;
if (findBand != null) {
// It can be null if the device supports continuous find mode
// If that's the case, this function shouldn't even have been called
for(int len : findBand.getOnOffSequence())
findDeviceInterval += len;
if(findBand.getRepeat() > 0)
findDeviceInterval *= findBand.getRepeat();
if(findDeviceInterval > 10000) // 10 seconds, about as long as Mi Fit allows
findDeviceInterval = 10000;
} else {
findDeviceInterval = 10000;
}
return findDeviceInterval;
return HuamiUtils.getFindDeviceInterval(getDevice(), supportsDeviceDefaultVibrationProfiles());
}
protected void sendFindDeviceCommand(boolean start) {
@@ -0,0 +1,38 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class HuamiUtils {
private HuamiUtils() {
// Utility class
}
public static int getFindDeviceInterval(final GBDevice device,
final boolean supportsDeviceDefaultVibrationProfiles) {
final VibrationProfile findBand = HuamiCoordinator.getVibrationProfile(
device.getAddress(),
HuamiVibrationPatternNotificationType.FIND_BAND,
supportsDeviceDefaultVibrationProfiles
);
int findDeviceInterval = 0;
if (findBand != null) {
// It can be null if the device supports continuous find mode
// If that's the case, this function shouldn't even have been called
for (int len : findBand.getOnOffSequence())
findDeviceInterval += len;
if (findBand.getRepeat() > 0)
findDeviceInterval *= findBand.getRepeat();
if (findDeviceInterval > 10000) // 10 seconds, about as long as Mi Fit allows
findDeviceInterval = 10000;
} else {
findDeviceInterval = 10000;
}
return findDeviceInterval;
}
}
@@ -19,8 +19,6 @@ 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.devices.huami.HuamiService.SUCCESS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_NAME;
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;
@@ -45,7 +43,6 @@ import android.content.Context;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.widget.Toast;
import androidx.annotation.Nullable;
@@ -61,7 +58,6 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
@@ -85,9 +81,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSilentMode;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMapsInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMusicInstallHandler;
@@ -138,6 +132,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCalendarService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService;
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.ZeppOsHttpService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLoyaltyCardService;
@@ -153,6 +148,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFtpServerService;
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.ZeppOsUserInfoService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsVoiceMemosService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWatchfaceService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWifiService;
@@ -160,7 +157,6 @@ 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.SilentMode;
public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferService.Callback {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsSupport.class);
@@ -200,6 +196,9 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
private final ZeppOsVoiceMemosService voiceMemosService = new ZeppOsVoiceMemosService(this);
private final ZeppOsMapsService mapsService = new ZeppOsMapsService(this, httpService);
private final ZeppOsMusicService musicService = new ZeppOsMusicService(this);
private final ZeppOsFindDeviceService findDeviceService = new ZeppOsFindDeviceService(this);
private final ZeppOsSilentModeService silentModeService = new ZeppOsSilentModeService(this);
private final ZeppOsUserInfoService userInfoService = new ZeppOsUserInfoService(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
@@ -231,6 +230,9 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
put(musicService.getEndpoint(), musicService);
put(voiceMemosService.getEndpoint(), voiceMemosService);
put(mapsService.getEndpoint(), mapsService);
put(findDeviceService.getEndpoint(), findDeviceService);
put(silentModeService.getEndpoint(), silentModeService);
put(userInfoService.getEndpoint(), userInfoService);
}};
public ZeppOsSupport() {
@@ -314,55 +316,19 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
voiceMemosService.requestList();
}
@Override
protected void acknowledgeFindPhone() {
LOG.info("Acknowledging find phone");
final byte[] cmd = new byte[]{FIND_PHONE_ACK, SUCCESS};
writeToChunked2021("ack find phone", CHUNKED2021_ENDPOINT_FIND_DEVICE, cmd, true);
}
protected void stopFindPhone() {
LOG.info("Stopping find phone");
writeToChunked2021("found phone", CHUNKED2021_ENDPOINT_FIND_DEVICE, FIND_PHONE_STOP_FROM_PHONE, true);
}
@Override
public void onFindDevice(final boolean start) {
if (getCoordinator().supportsContinuousFindDevice()) {
sendFindDeviceCommand(start);
} else {
// Vibrate band periodically
super.onFindDevice(start);
}
findDeviceService.onFindDevice(start);
}
@Override
protected void sendFindDeviceCommand(boolean start) {
final byte findBandCommand = start ? FIND_BAND_START : FIND_BAND_STOP_FROM_PHONE;
LOG.info("Sending find band {}", start);
try {
final TransactionBuilder builder = performInitialized("find huami 2021");
writeToChunked2021(builder, CHUNKED2021_ENDPOINT_FIND_DEVICE, findBandCommand, true);
builder.queue(getQueue());
} catch (IOException e) {
LOG.error("error while sending find Huami 2021 device command", e);
}
findDeviceService.sendFindDeviceCommand(start);
}
@Override
public void onFindPhone(final boolean start) {
LOG.info("Find phone: {}", start);
findPhoneStarted = start;
if (!start) {
stopFindPhone();
}
findDeviceService.onFindPhone(start);
}
@Override
@@ -504,64 +470,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
.write(builder);
}
@Override
protected ZeppOsSupport setUserInfo(final TransactionBuilder builder) {
LOG.info("Attempting to set user info...");
final Prefs prefs = GBApplication.getPrefs();
final Prefs devicePrefs = getDevicePrefs();
final String alias = prefs.getString(PREF_USER_NAME, null);
final ActivityUser activityUser = new ActivityUser();
final int height = activityUser.getHeightCm();
final int weight = activityUser.getWeightKg();
final LocalDate dateOfBirth = activityUser.getDateOfBirth();
final int birthYear = dateOfBirth.getYear();
final byte birthMonth = (byte) dateOfBirth.getMonthValue();
final byte birthDay = (byte) dateOfBirth.getDayOfMonth();
final String region = devicePrefs.getString(DeviceSettingsPreferenceConst.PREF_DEVICE_REGION, "unknown");
if (alias == null || weight == 0 || height == 0 || birthYear == 0) {
LOG.warn("Unable to set user info, make sure it is set up");
return this;
}
byte genderByte = 2; // other
switch (activityUser.getGender()) {
case ActivityUser.GENDER_MALE:
genderByte = 0;
break;
case ActivityUser.GENDER_FEMALE:
genderByte = 1;
}
final int userid = alias.hashCode();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write(USER_INFO_CMD_SET);
baos.write(new byte[]{0x4f, 0x07, 0x00, 0x00});
baos.write(fromUint16(birthYear));
baos.write(birthMonth);
baos.write(birthDay);
baos.write(genderByte);
baos.write(fromUint16(height));
baos.write(fromUint16(weight * 200));
baos.write(BLETypeConversions.fromUint64(userid));
baos.write(region.getBytes(StandardCharsets.UTF_8));
baos.write(0);
baos.write(0x09); // TODO ?
baos.write(alias.getBytes(StandardCharsets.UTF_8));
baos.write((byte) 0);
writeToChunked2021(builder, Huami2021Service.CHUNKED2021_ENDPOINT_USER_INFO, baos.toByteArray(), true);
} catch (final Exception e) {
LOG.error("Failed to send user info", e);
}
return this;
}
@Override
protected ZeppOsSupport setPassword(final TransactionBuilder builder) {
final boolean passwordEnabled = HuamiCoordinator.getPasswordEnabled(gbDevice.getAddress());
@@ -1310,10 +1218,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
setVibrationPattern(builder, HuamiConst.PREF_HUAMI_VIBRATION_PROFILE_PREFIX + typeKey);
}
// TODO move these to a service
cannedMessagesService.requestCannedMessages(builder);
alarmsService.requestAlarms(builder);
for (AbstractZeppOsService service : mServiceMap.values()) {
if (mSupportedServices.contains(service.getEndpoint())) {
// Only initialize supported services
@@ -1321,11 +1225,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
}
}
if (coordinator.supportsBluetoothPhoneCalls(gbDevice)) {
phoneService.requestCapabilities(builder);
phoneService.requestEnabled(builder);
}
builder.queue(getQueue());
} catch (Exception e) {
LOG.error("failed initializing device", e);
@@ -1465,18 +1364,12 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
case CHUNKED2021_ENDPOINT_WORKOUT:
handle2021Workout(payload);
return;
case CHUNKED2021_ENDPOINT_FIND_DEVICE:
handle2021FindDevice(payload);
return;
case CHUNKED2021_ENDPOINT_HEARTRATE:
handle2021HeartRate(payload);
return;
case CHUNKED2021_ENDPOINT_CONNECTION:
handle2021Connection(payload);
return;
case CHUNKED2021_ENDPOINT_USER_INFO:
handle2021UserInfo(payload);
return;
case CHUNKED2021_ENDPOINT_STEPS:
handle2021Steps(payload);
return;
@@ -1486,9 +1379,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
case CHUNKED2021_ENDPOINT_BATTERY:
handle2021Battery(payload);
return;
case CHUNKED2021_ENDPOINT_SILENT_MODE:
handle2021SilentMode(payload);
return;
default:
LOG.warn("Unhandled 2021 payload {}", String.format("0x%04x", type));
}
@@ -1532,56 +1422,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
}
}
/**
* A handler to schedule the find phone event.
*/
private final Handler findPhoneHandler = new Handler();
private boolean findPhoneStarted;
protected void handle2021FindDevice(final byte[] payload) {
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
switch (payload[0]) {
case FIND_BAND_ACK:
LOG.info("Band acknowledged find band command");
return;
case FIND_PHONE_START:
LOG.info("Find Phone Start");
acknowledgeFindPhone(); // FIXME: Premature, but the band will only send the mode after we ack
// Delay the find phone start, because we might get the FIND_PHONE_MODE
findPhoneHandler.postDelayed(() -> {
findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
evaluateGBDeviceEvent(findPhoneEvent);
}, 1500);
break;
case FIND_BAND_STOP_FROM_BAND:
LOG.info("Find Band Stop from Band");
break;
case FIND_PHONE_STOP_FROM_BAND:
LOG.info("Find Phone Stop");
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
evaluateGBDeviceEvent(findPhoneEvent);
break;
case FIND_PHONE_MODE:
findPhoneHandler.removeCallbacksAndMessages(null);
final int mode = payload[1] & 0xff; // 0 to only vibrate, 1 to ring
LOG.info("Find Phone Mode: {}", mode);
if (findPhoneStarted) {
// Already started, just change the mode
findPhoneEvent.event = mode == 1 ? GBDeviceEventFindPhone.Event.RING : GBDeviceEventFindPhone.Event.VIBRATE;
} else {
findPhoneEvent.event = mode == 1 ? GBDeviceEventFindPhone.Event.START : GBDeviceEventFindPhone.Event.START_VIBRATE;
}
evaluateGBDeviceEvent(findPhoneEvent);
break;
default:
LOG.warn("Unexpected find phone byte {}", String.format("0x%02x", payload[0]));
}
}
protected void handle2021HeartRate(final byte[] payload) {
switch (payload[0]) {
case HEART_RATE_CMD_REALTIME_ACK:
@@ -1636,17 +1476,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
LOG.warn("Unexpected connection payload byte {}", String.format("0x%02x", payload[0]));
}
protected void handle2021UserInfo(final byte[] payload) {
//noinspection SwitchStatementWithTooFewBranches
switch (payload[0]) {
case USER_INFO_CMD_SET_ACK:
LOG.info("Got user info set ack, status = {}", payload[1]);
return;
}
LOG.warn("Unexpected user info payload byte {}", String.format("0x%02x", payload[0]));
}
protected void handle2021Steps(final byte[] payload) {
switch (payload[0]) {
case STEPS_CMD_REPLY:
@@ -1698,45 +1527,6 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
handleGBDeviceEvent(batteryInfo.toDeviceEvent());
}
protected void handle2021SilentMode(final byte[] payload) {
switch (payload[0]) {
case SILENT_MODE_CMD_NOTIFY_BAND_ACK:
LOG.info("Band acknowledged current phone silent mode, status = {}", payload[1]);
return;
case SILENT_MODE_CMD_QUERY:
LOG.info("Got silent mode query from band");
sendPhoneSilentMode(SilentMode.isPhoneInSilenceMode(getDevice().getAddress()));
return;
case SILENT_MODE_CMD_SET:
LOG.info("Band setting silent mode = {}", payload[1]);
final boolean silentModeEnabled = (payload[1] == 1);
ackSilentModeSet();
sendPhoneSilentMode(silentModeEnabled);
evaluateGBDeviceEvent(new GBDeviceEventSilentMode(silentModeEnabled));
return;
default:
LOG.warn("Unexpected silent mode payload byte {}", String.format("0x%02x", payload[0]));
}
}
private void ackSilentModeSet() {
writeToChunked2021(
"ack silent mode set",
CHUNKED2021_ENDPOINT_SILENT_MODE,
new byte[]{SILENT_MODE_CMD_ACK, 0x01},
false
);
}
private void sendPhoneSilentMode(final boolean enabled) {
writeToChunked2021(
"send phone silent mode to band",
CHUNKED2021_ENDPOINT_SILENT_MODE,
new byte[]{SILENT_MODE_CMD_NOTIFY_BAND, bool(enabled)},
false
);
}
@Override
public void onFileUploadFinish(final boolean success) {
LOG.warn("Unexpected file upload finish: {}", success);
@@ -98,6 +98,11 @@ public class ZeppOsAlarmsService extends AbstractZeppOsService {
}
}
@Override
public void initialize(final TransactionBuilder builder) {
requestAlarms(builder);
}
private void requestAlarms() {
try {
final TransactionBuilder builder = new TransactionBuilder("request alarms");
@@ -98,6 +98,11 @@ public class ZeppOsCannedMessagesService extends AbstractZeppOsService {
}
}
@Override
public void initialize(final TransactionBuilder builder) {
requestCannedMessages(builder);
}
public void setCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
if (cannedMessagesSpec.type != CannedMessagesSpec.TYPE_GENERIC) {
LOG.warn("Got unsupported canned messages type: {}", cannedMessagesSpec.type);
@@ -0,0 +1,168 @@
/* 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.os.Handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
public class ZeppOsFindDeviceService extends AbstractZeppOsService {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsFindDeviceService.class);
private static final short ENDPOINT = 0x001a;
public static final byte CMD_CAPABILITIES_REQUEST = 0x01;
public static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
public static final byte FIND_BAND_START = 0x03;
public static final byte FIND_BAND_ACK = 0x04;
public static final byte FIND_BAND_STOP_FROM_PHONE = 0x06;
public static final byte FIND_BAND_STOP_FROM_BAND = 0x07;
public static final byte FIND_PHONE_START = 0x11;
public static final byte FIND_PHONE_ACK = 0x12;
public static final byte FIND_PHONE_STOP_FROM_BAND = 0x13;
public static final byte FIND_PHONE_STOP_FROM_PHONE = 0x14;
public static final byte FIND_PHONE_MODE = 0x15;
private final Handler findWatchHandler = new Handler();
private boolean findingWatch = false;
private final Handler findPhoneHandler = new Handler();
private boolean findPhoneStarted;
public ZeppOsFindDeviceService(final ZeppOsSupport support) {
super(support, true);
}
@Override
public short getEndpoint() {
return ENDPOINT;
}
@Override
public void handlePayload(final byte[] payload) {
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
switch (payload[0]) {
case CMD_CAPABILITIES_RESPONSE:
// mb7: 02:01:01
// active2 / gtr4: 02:01:02
break;
case FIND_BAND_ACK:
LOG.info("Band acknowledged find band command");
final boolean supportsContinuous = getCoordinator().supportsContinuousFindDevice();
if (findingWatch && !supportsContinuous) {
findWatchHandler.postDelayed(() -> {
LOG.debug("Triggering find device vibration");
sendFindDeviceCommand(true);
}, HuamiUtils.getFindDeviceInterval(getSupport().getDevice(), true));
}
return;
case FIND_PHONE_START:
LOG.info("Find Phone Start");
acknowledgeFindPhone(); // FIXME: Premature, but the band will only send the mode after we ack
// Delay the find phone start, because we might get the FIND_PHONE_MODE
findPhoneHandler.postDelayed(() -> {
findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
evaluateGBDeviceEvent(findPhoneEvent);
}, 1500);
break;
case FIND_BAND_STOP_FROM_BAND:
LOG.info("Find Band Stop from Band");
break;
case FIND_PHONE_STOP_FROM_BAND:
LOG.info("Find Phone Stop");
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
evaluateGBDeviceEvent(findPhoneEvent);
break;
case FIND_PHONE_MODE:
findPhoneHandler.removeCallbacksAndMessages(null);
final int mode = payload[1] & 0xff; // 0 to only vibrate, 1 to ring
LOG.info("Find Phone Mode: {}", mode);
if (findPhoneStarted) {
// Already started, just change the mode
findPhoneEvent.event = mode == 1 ? GBDeviceEventFindPhone.Event.RING : GBDeviceEventFindPhone.Event.VIBRATE;
} else {
findPhoneEvent.event = mode == 1 ? GBDeviceEventFindPhone.Event.START : GBDeviceEventFindPhone.Event.START_VIBRATE;
}
evaluateGBDeviceEvent(findPhoneEvent);
break;
default:
LOG.warn("Unexpected find phone byte {}", String.format("0x%02x", payload[0]));
}
}
@Override
public void initialize(final TransactionBuilder builder) {
write(builder, CMD_CAPABILITIES_REQUEST);
}
@Override
public void dispose() {
findWatchHandler.removeCallbacksAndMessages(null);
findPhoneHandler.removeCallbacksAndMessages(null);
}
public void onFindDevice(final boolean start) {
LOG.debug("onFindDevice {}", start);
findWatchHandler.removeCallbacksAndMessages(null);
findingWatch = start;
sendFindDeviceCommand(start);
}
public void onFindPhone(final boolean start) {
LOG.info("Find phone: {}", start);
findPhoneStarted = start;
if (!start) {
stopFindPhone();
}
}
// FIXME should be private?
public void sendFindDeviceCommand(final boolean start) {
final byte findBandCommand = start ? FIND_BAND_START : FIND_BAND_STOP_FROM_PHONE;
LOG.info("Sending find band {}", start);
write("find huami 2021", findBandCommand);
}
private void acknowledgeFindPhone() {
LOG.info("Acknowledging find phone");
write("ack find phone", new byte[]{FIND_PHONE_ACK, 0x01 /* success */});
}
private void stopFindPhone() {
LOG.info("Stopping find phone");
write("found phone", FIND_PHONE_STOP_FROM_PHONE);
}
}
@@ -131,6 +131,10 @@ public class ZeppOsPhoneService extends AbstractZeppOsService {
@Override
public void initialize(final TransactionBuilder builder) {
if (getCoordinator().supportsBluetoothPhoneCalls(getSupport().getDevice())) {
requestCapabilities(builder);
requestEnabled(builder);
}
}
public boolean isSupported() {
@@ -0,0 +1,83 @@
/* 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSilentMode;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.util.SilentMode;
public class ZeppOsSilentModeService extends AbstractZeppOsService {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsSilentModeService.class);
private static final short ENDPOINT = 0x003b;
public static final byte SILENT_MODE_CMD_CAPABILITIES_REQUEST = 0x01;
public static final byte SILENT_MODE_CMD_CAPABILITIES_RESPONSE = 0x02;
// Notify silent mode, from phone
public static final byte SILENT_MODE_CMD_NOTIFY_BAND = 0x03;
public static final byte SILENT_MODE_CMD_NOTIFY_BAND_ACK = 0x04;
// Query silent mode on phone, from band
public static final byte SILENT_MODE_CMD_QUERY = 0x05;
public static final byte SILENT_MODE_CMD_REPLY = 0x06;
// Set silent mode on phone, from band
// After this, phone sends ACK + NOTIFY
public static final byte SILENT_MODE_CMD_SET = 0x07;
public static final byte SILENT_MODE_CMD_ACK = 0x08;
public ZeppOsSilentModeService(final ZeppOsSupport support) {
super(support, true);
}
@Override
public short getEndpoint() {
return ENDPOINT;
}
@Override
public void handlePayload(final byte[] payload) {
switch (payload[0]) {
case SILENT_MODE_CMD_NOTIFY_BAND_ACK:
LOG.info("Band acknowledged current phone silent mode, status = {}", payload[1]);
return;
case SILENT_MODE_CMD_QUERY:
LOG.info("Got silent mode query from band");
sendPhoneSilentMode(SilentMode.isPhoneInSilenceMode(getSupport().getDevice().getAddress()));
return;
case SILENT_MODE_CMD_SET:
LOG.info("Band setting silent mode = {}", payload[1]);
final boolean silentModeEnabled = (payload[1] == 1);
ackSilentModeSet();
sendPhoneSilentMode(silentModeEnabled);
evaluateGBDeviceEvent(new GBDeviceEventSilentMode(silentModeEnabled));
return;
default:
LOG.warn("Unexpected silent mode payload byte {}", String.format("0x%02x", payload[0]));
}
}
private void ackSilentModeSet() {
write("ack silent mode set", new byte[]{SILENT_MODE_CMD_ACK, 0x01});
}
private void sendPhoneSilentMode(final boolean enabled) {
write("send phone silent mode to band", new byte[]{SILENT_MODE_CMD_NOTIFY_BAND, bool(enabled)});
}
}
@@ -0,0 +1,148 @@
/* 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 nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_REGION;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_DATE_OF_BIRTH;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_GENDER;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_HEIGHT_CM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_NAME;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_WEIGHT_KG;
import static nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions.fromUint16;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
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.Prefs;
public class ZeppOsUserInfoService extends AbstractZeppOsService {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsUserInfoService.class);
private static final short ENDPOINT = 0x0017;
public static final byte USER_INFO_CMD_SET = 0x01;
public static final byte USER_INFO_CMD_SET_ACK = 0x02;
public ZeppOsUserInfoService(final ZeppOsSupport support) {
super(support, true);
}
@Override
public short getEndpoint() {
return ENDPOINT;
}
@Override
public void handlePayload(final byte[] payload) {
//noinspection SwitchStatementWithTooFewBranches
switch (payload[0]) {
case USER_INFO_CMD_SET_ACK:
LOG.info("Got user info set ack, status = {}", payload[1]);
return;
}
LOG.warn("Unexpected user info payload byte {}", String.format("0x%02x", payload[0]));
}
@Override
public void initialize(final TransactionBuilder builder) {
setUserInfo(builder);
}
@Override
public boolean onSendConfiguration(final String config, final Prefs prefs) {
switch (config) {
case PREF_USER_DATE_OF_BIRTH:
case PREF_USER_NAME:
case PREF_USER_WEIGHT_KG:
case PREF_USER_HEIGHT_CM:
case PREF_USER_GENDER:
case PREF_DEVICE_REGION:
final TransactionBuilder builder = getSupport().createTransactionBuilder("set user info");
setUserInfo(builder);
builder.queue(getSupport().getQueue());
return true;
}
return false;
}
public void setUserInfo(final TransactionBuilder builder) {
LOG.info("Attempting to set user info...");
final Prefs prefs = GBApplication.getPrefs();
final Prefs devicePrefs = getDevicePrefs();
final String alias = prefs.getString(PREF_USER_NAME, null);
final ActivityUser activityUser = new ActivityUser();
final int height = activityUser.getHeightCm();
final int weight = activityUser.getWeightKg();
final LocalDate dateOfBirth = activityUser.getDateOfBirth();
final int birthYear = dateOfBirth.getYear();
final byte birthMonth = (byte) dateOfBirth.getMonthValue();
final byte birthDay = (byte) dateOfBirth.getDayOfMonth();
final String region = devicePrefs.getString(PREF_DEVICE_REGION, "unknown");
if (alias == null || weight == 0 || height == 0 || birthYear == 0) {
LOG.warn("Unable to set user info, make sure it is set up");
return;
}
byte genderByte = 2; // other
switch (activityUser.getGender()) {
case ActivityUser.GENDER_MALE:
genderByte = 0;
break;
case ActivityUser.GENDER_FEMALE:
genderByte = 1;
}
final int userid = alias.hashCode();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write(USER_INFO_CMD_SET);
baos.write(new byte[]{0x4f, 0x07, 0x00, 0x00});
baos.write(fromUint16(birthYear));
baos.write(birthMonth);
baos.write(birthDay);
baos.write(genderByte);
baos.write(fromUint16(height));
baos.write(fromUint16(weight * 200));
baos.write(BLETypeConversions.fromUint64(userid));
baos.write(region.getBytes(StandardCharsets.UTF_8));
baos.write(0);
baos.write(0x09); // TODO ?
baos.write(alias.getBytes(StandardCharsets.UTF_8));
baos.write((byte) 0);
write(builder, baos.toByteArray());
} catch (final Exception e) {
LOG.error("Failed to send user info", e);
}
}
}