1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-24 07:38:45 +02:00

add support for Hama Fit6900 watch

This commit is contained in:
enoint 2024-06-15 11:37:13 +02:00 committed by José Rebelo
parent 7cafbc2002
commit 2ec568bec7
6 changed files with 1201 additions and 0 deletions

View File

@ -0,0 +1,28 @@
/*
Copyright (C) 2024 enoint
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 <http://www.gnu.org/licenses/>.
*/
package nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900;
import java.util.UUID;
public class HamaFit6900Constants {
public static final UUID UUID_SERVICE_RXTX = UUID.fromString("c3e6fea0-e966-1000-8000-be99c223df6a");
public static final UUID UUID_CHARACTERISTIC_TX = UUID.fromString("c3e6fea1-e966-1000-8000-be99c223df6a");
public static final UUID UUID_CHARACTERISTIC_RX = UUID.fromString("c3e6fea2-e966-1000-8000-be99c223df6a");
}

View File

@ -0,0 +1,105 @@
/*
Copyright (C) 2024 enoint
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 <http://www.gnu.org/licenses/>.
*/
package nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.hama.fit6900.HamaFit6900DeviceSupport;
public final class HamaFit6900DeviceCoordinator extends AbstractBLEDeviceCoordinator {
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("Fit6900");
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_NONE;
}
@Override
public int getAlarmSlotCount(GBDevice device) {
return 5;
}
@Override
public boolean supportsFindDevice() {
return true;
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_allow_accept_reject_calls, // reject only
R.xml.devicesettings_camera_remote,
R.xml.devicesettings_find_phone,
R.xml.devicesettings_liftwrist_display_no_on,
R.xml.devicesettings_notifications_enable,
R.xml.devicesettings_timeformat,
R.xml.devicesettings_transliteration,
R.xml.devicesettings_donotdisturb_no_auto,
R.xml.devicesettings_autoheartrate,
R.xml.devicesettings_hydration_reminder
};
}
@Override
public String[] getSupportedLanguageSettings(GBDevice device) {
return new String[]{
"auto",
"en_US",
"es_ES",
"de_DE",
"it_IT",
"fr_FR",
"sv_SE"
};
}
@Override
public String getManufacturer() {
return "Hama";
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return HamaFit6900DeviceSupport.class;
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_hama_fit6900;
}
}

View File

@ -0,0 +1,440 @@
/*
Copyright (C) 2024 enoint
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 <http://www.gnu.org/licenses/>.
*/
package nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public final class Message {
private static byte encodeBoolean(boolean value) {
return (byte) ((value) ? 1 : 0);
}
private static void encodeInt16(byte[] data, int offset, int value) {
data[offset + 0] = (byte) (value & 0xFF);
data[offset + 1] = (byte) ((value >> 8) & 0xFF);
}
private static void encodeInt32(byte[] data, int offset, int value) {
data[offset + 0] = (byte) (value & 0xFF);
data[offset + 1] = (byte) ((value >> 8) & 0xFF);
data[offset + 2] = (byte) ((value >> 16) & 0xFF);
data[offset + 3] = (byte) ((value >> 24) & 0xFF);
}
private static void encodeInt32Goal(byte[] data, int offset, int value) {
encodeInt32(data, offset, value);
data[offset + 3] = 0;
}
private static byte encodeTimeFormat(TimeFormat tf) {
return (byte) ((tf == TimeFormat.Format12H) ? 1 : 0);
}
public static int decodeInt32(byte[] data, int offset) {
return ((data[offset] & 0xFF) << 24) | ((data[offset + 1] & 0xFF) << 16) | ((data[offset + 2] & 0xFF) << 8) | (data[offset + 3] & 0xFF);
}
private static byte[] byteArrayAdd(byte[] b1, byte[] b2) {
if (b2 == null || b2.length == 0)
return b1;
byte[] result = new byte[b1.length + b2.length];
System.arraycopy(b1, 0, result, 0, b1.length);
System.arraycopy(b2, 0, result, b1.length, b2.length);
return result;
}
public static int calculateCrc(byte[] data) {
int crc = 0xFF;
for (byte b : data) {
crc ^= b & 0xFF;
for (int k = 0; k < 8; k++) {
if ((crc & 1) != 0) {
crc = (crc >> 1) ^ 184;
} else {
crc = (crc >> 1);
}
}
}
return crc;
}
private static final byte HEADER_MAGIC_NUMBER = (byte) 186;
private static final byte PROTOCOL_VERSION_0 = (byte) 0;
private static byte[] encodeMessage(byte[] commandData) {
// msgType | 1: single message or end of multi-part message, 3: one part of a multi-part message
byte msgType = 1;
byte unknown1 = 0; // 1 bit
byte unknown2 = 0; // 4 bits
int length = commandData.length;
int crc = calculateCrc(commandData);
int msgCounter = 0; // returned with the response
byte[] header = new byte[8];
header[0] = HEADER_MAGIC_NUMBER;
header[1] = (byte) ((msgType << 5) | (unknown1 << 4) | unknown2);
header[2] = (byte) ((length >> 8) & 0xFF);
header[3] = (byte) (length & 0xFF);
header[4] = (byte) ((crc >> 8) & 0xFF);
header[5] = (byte) (crc & 0xFF);
header[6] = (byte) ((msgCounter >> 8) & 0xFF);
header[7] = (byte) (msgCounter & 0xFF);
return byteArrayAdd(header, commandData);
}
private static byte[] encodeCommand(byte cmd, byte cmdKey, byte[] argsData) {
int length = (argsData != null) ? argsData.length : 0;
byte[] header = new byte[5];
header[0] = cmd;
header[1] = PROTOCOL_VERSION_0;
header[2] = cmdKey;
header[3] = (byte) ((length >> 8) & 1);
header[4] = (byte) (length & 0xFF);
return byteArrayAdd(header, argsData);
}
public static byte[] encodeCommandMessage(int cmd, int cmdKey, byte[] cmdArgsData) {
assert (cmd > 0);
assert (cmd <= 0xFF);
assert (cmdKey > 0);
assert (cmdKey <= 0xFF);
return encodeMessage(encodeCommand((byte) cmd, (byte) cmdKey, cmdArgsData));
}
public static class CommandMessage { // decoded message
public byte msgType;
public int cmd;
public int key;
public byte[] cmdArgs;
}
public static CommandMessage decodeCommandMessage(byte[] data) {
final int MESSAGE_HEADER_SIZE = 8;
final int COMMAND_HEADER_SIZE = 5;
if (data[0] != HEADER_MAGIC_NUMBER) {
return null;
}
byte messageType = (byte) (data[1] >> 5);
int messageLength = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF);
int crcReceived = ((data[4] & 0xFF) << 8) | (data[5] & 0xFF);
//int msgCounter = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
if (data.length != MESSAGE_HEADER_SIZE + messageLength) {
return null;
}
{
byte[] commandData = new byte[messageLength];
System.arraycopy(data, MESSAGE_HEADER_SIZE, commandData, 0, messageLength);
int crc = calculateCrc(commandData);
if (crc != crcReceived) {
return null;
}
}
int cmd = data[MESSAGE_HEADER_SIZE + 0] & 0xFF;
int version = data[MESSAGE_HEADER_SIZE + 1] & 0xFF;
int key = data[MESSAGE_HEADER_SIZE + 2] & 0xFF;
int commandLength = ((data[MESSAGE_HEADER_SIZE + 3] & 1) << 8) | (data[MESSAGE_HEADER_SIZE + 4] & 0xFF);
if (version != PROTOCOL_VERSION_0) {
return null;
}
if (data.length != MESSAGE_HEADER_SIZE + COMMAND_HEADER_SIZE + commandLength) {
return null;
}
byte[] commandArgs = new byte[commandLength];
System.arraycopy(data, MESSAGE_HEADER_SIZE + COMMAND_HEADER_SIZE, commandArgs, 0, commandLength);
CommandMessage result = new CommandMessage();
result.msgType = messageType;
result.cmd = cmd;
result.key = key;
result.cmdArgs = commandArgs;
return result;
}
public static byte[] encodeGetFirmwareVersion() {
return encodeCommandMessage(1, 18, null);
}
public static byte[] encodeGetBatteryStatus() {
return encodeCommandMessage(4, 64, null);
}
private static void encodeDateTime(byte[] data, int dataOffset, int year, int month, int day, int hour, int minute, int second) {
data[dataOffset + 0] = (byte) ((year % 100) & 0xFF);
data[dataOffset + 1] = (byte) (month & 0xFF);
data[dataOffset + 2] = (byte) (day & 0xFF);
data[dataOffset + 3] = (byte) (hour & 0xFF);
data[dataOffset + 4] = (byte) (minute & 0xFF);
data[dataOffset + 5] = (byte) (second & 0xFF);
}
public enum TimeFormat {
Format12H,
Format24H
}
public static byte[] encodeSetDateTime(Calendar dt, TimeFormat timeFormat) {
byte[] args = new byte[7];
encodeDateTime(args, 0, dt.get(Calendar.YEAR), dt.get(Calendar.MONTH) + 1,
dt.get(Calendar.DAY_OF_MONTH), dt.get(Calendar.HOUR_OF_DAY), dt.get(Calendar.MINUTE), dt.get(Calendar.SECOND));
// time format value is optional. shorter message is also accepted
args[6] = encodeTimeFormat(timeFormat);
return encodeCommandMessage(2, 32, args);
}
public enum Gender {
FEMALE,
MALE
}
public static byte[] encodeSetUserInfo(Gender gender, int age, int heightCm, int weightKg, int stepsGoal) {
byte[] args = new byte[8];
args[0] = (byte) ((gender == Gender.MALE) ? 1 : 0);
args[1] = (byte) (age & 0xFF);
args[2] = (byte) (heightCm & 0xFF);
args[3] = (byte) (weightKg & 0xFF);
encodeInt32Goal(args, 4, stepsGoal);
return encodeCommandMessage(2, 35, args);
}
public static byte[] encodeFindDevice() {
return encodeCommandMessage(5, 80, null);
}
public enum NotificationType {
INCOMING_CALL(0), // shows popup with hang up button
SMS(1),
MQQ(2),
WEIXIN(3),
FACEBOOK(4),
TWITTER(6),
WHATSAPP(7),
INSTAGRAM(8),
LINKEDIN(9),
CALL_REJECT(15), // closes INCOMING_CALL notification
CALL_ACCEPT(16), // closes INCOMING call notification
UNKNOWN(255);
private final byte value;
NotificationType(final int value) {
this.value = (byte) value;
}
public byte getValue() {
return this.value;
}
}
public static byte[] encodeShowNotification(NotificationType type, String text) {
final int TEXT_LENGTH_MAX = 64;
text = text.trim();
if (text.length() > TEXT_LENGTH_MAX) {
text = StringUtils.truncate(text, TEXT_LENGTH_MAX);
text = text.trim(); // trim again so text is centered on screen
}
byte[] args = byteArrayAdd(new byte[]{type.getValue()}, text.getBytes(StandardCharsets.UTF_16LE));
return encodeCommandMessage(6, 96, args);
}
public static byte[] encodeSetAlarms(ArrayList<? extends Alarm> alarms) {
final int ENTRY_COUNT = 5;
final int ENTRY_SIZE = 5;
byte[] args = new byte[ENTRY_COUNT * ENTRY_SIZE];
Arrays.fill(args, (byte) 0);
int offset = 0;
for (Alarm alarm : alarms) {
// When all properties of an alarm are 0, it will not be listed in the watch UI.
// Disabled alarms with non-0 properties will be shown in disabled state.
// Show only enabled:
if (alarm.getEnabled() && !alarm.getUnused()) {
args[offset + 0] = (byte) alarm.getHour();
args[offset + 1] = (byte) alarm.getMinute();
args[offset + 2] = (byte) alarm.getRepetition();
args[offset + 3] = (byte) 1;
args[offset + 4] = encodeBoolean(alarm.getEnabled());
offset += ENTRY_SIZE;
}
}
return encodeCommandMessage(2, 33, args);
}
private static final Map<String, Integer> LANGUAGES = new HashMap<String, Integer>() {{
put("en", 1);
put("es", 3);
put("de", 4);
put("it", 5);
put("fr", 6);
put("sv", 18);
put("ru", 2);
put("pt", 7);
put("pl", 8);
put("nl", 9);
put("el", 10);
put("tr", 11);
put("ro", 12);
put("ja", 13);
put("he", 15);
put("da", 16);
put("sr", 17);
put("cs", 19);
put("sk", 20);
put("hu", 21);
put("ar", 22);
put("bg", 23);
put("th", 24);
put("uk", 25);
put("fi", 26);
put("nb", 27);
put("ko", 28);
put("id", 29);
put("lv", 30);
put("lt", 31);
put("et", 32);
put("my", 33);
put("vi", 34);
put("hr", 35);
}};
private static int resolveLanguageId(String language_, String country_) {
String language = language_.toLowerCase();
String country = country_.toLowerCase();
if (language.equals("zh")) {
switch (country) {
case "cn":
return 0;
case "tw":
case "hk":
case "mo":
return 14;
}
} else if (language.equals("pt") && country.equals("br")) {
return 36;
} else {
Integer languageId = LANGUAGES.get(language);
if (languageId != null)
return languageId;
}
return LANGUAGES.get("en");
}
public static byte[] encodeSetSystemData(String lang, String country, TimeFormat timeFormat) {
byte[] args = new byte[4];
args[0] = (byte) resolveLanguageId(lang, country);
args[1] = encodeTimeFormat(timeFormat);
args[2] = (byte) 60; // screen. unclear
args[3] = (byte) 0; // pair. 1 has no effect
return encodeCommandMessage(2, 39, args);
}
public static byte[] encodeSetDoNotDisturb(boolean enable, int startHour, int startMinute, int endHour, int endMinute) {
byte[] args = new byte[5];
args[0] = encodeBoolean(enable);
args[1] = (byte) startHour;
args[2] = (byte) startMinute;
args[3] = (byte) endHour;
args[4] = (byte) endMinute;
return encodeCommandMessage(6, 100, args);
}
public static byte[] encodeSetUnit(boolean isMetric) {
byte[] args = new byte[2];
args[0] = args[1] = (byte) ((isMetric) ? 0 : 1); // 0: metric, 1: imperial
return encodeCommandMessage(2, 1, args);
}
public static byte[] encodeSetLiftWristDisplayOn(boolean enable) {
byte[] args = new byte[3];
args[0] = (byte) 1; // hand
args[1] = encodeBoolean(enable); // raise
args[2] = encodeBoolean(enable); // wrist
return encodeCommandMessage(4, 74, args);
}
public static byte[] encodeSetAutoHeartRate(boolean enable, int startHour, int startMinute, int endHour, int endMinute, int intervalMinutes) {
byte[] args = new byte[7];
args[0] = encodeBoolean(enable);
args[1] = (byte) startHour;
args[2] = (byte) startMinute;
args[3] = (byte) endHour;
args[4] = (byte) endMinute;
encodeInt16(args, 5, intervalMinutes);
return encodeCommandMessage(9, 146, args);
}
public static byte[] encodeSetHydrationReminder(boolean enable, int startHour, int startMinute, int endHour, int endMinute, int intervalMinutes) {
byte[] args = new byte[8];
args[0] = encodeBoolean(enable);
args[1] = (byte) startHour;
args[2] = (byte) startMinute;
args[3] = (byte) endHour;
args[4] = (byte) endMinute;
args[5] = (byte) 0x7F; // repeat: bitmask for days of week, bit 0=Monday
encodeInt16(args, 6, intervalMinutes);
return encodeCommandMessage(2, 40, args);
}
public static byte[] encodeFactoryReset() {
return encodeCommandMessage(2, 199, null);
}
}

View File

@ -74,6 +74,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivoactive.Ga
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivomove.GarminVivomoveHrCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivomove.GarminVivomoveStyleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivosmart.GarminVivosmart5Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900.HamaFit6900DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.EXRIZUK8Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.MakibesF68Coordinator;
@ -436,6 +437,7 @@ public enum DeviceType {
SONY_WENA_3(SonyWena3Coordinator.class),
FEMOMETER_VINCA2(FemometerVinca2DeviceCoordinator.class),
PIXOO(PixooCoordinator.class),
HAMA_FIT6900(HamaFit6900DeviceCoordinator.class),
SCANNABLE(ScannableDeviceCoordinator.class),
CYCLING_SENSOR(CyclingSensorCoordinator.class),
TEST(TestDeviceCoordinator.class);

View File

@ -0,0 +1,625 @@
/*
Copyright (C) 2024 enoint
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 <http://www.gnu.org/licenses/>.
*/
package nodomain.freeyourgadget.gadgetbridge.service.devices.hama.fit6900;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_FIND_PHONE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_FIND_PHONE_DURATION;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.SharedPreferences;
import android.os.Handler;
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.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900.HamaFit6900Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900.Message;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
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.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public final class HamaFit6900DeviceSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(HamaFit6900DeviceSupport.class);
private BluetoothGattCharacteristic writeCharacteristic;
private int notificationCount = 0;
private final Handler findPhoneStopNotificationHandler = new Handler();
public HamaFit6900DeviceSupport() {
super(LOG);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
addSupportedService(HamaFit6900Constants.UUID_SERVICE_RXTX);
}
@Override
public TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
writeCharacteristic = getCharacteristic(HamaFit6900Constants.UUID_CHARACTERISTIC_TX);
builder.notify(getCharacteristic(HamaFit6900Constants.UUID_CHARACTERISTIC_RX), true);
builder.setCallback(this);
builder.write(writeCharacteristic, Message.encodeGetBatteryStatus());
builder.write(writeCharacteristic, Message.encodeGetFirmwareVersion());
if (GBApplication.getPrefs().getBoolean("datetime_synconconnect", true)) {
builder.write(writeCharacteristic, makeSetDateTimeMessage());
}
// sync all preferences to device
builder.write(writeCharacteristic, makeSetSystemDataMessage());
builder.write(writeCharacteristic, makeSetUnitMessage());
builder.write(writeCharacteristic, makeSetUserInfoMessage());
builder.write(writeCharacteristic, makeSetAutoHeartRate());
builder.write(writeCharacteristic, makeSetDoNotDisturbMessage());
builder.write(writeCharacteristic, makeSetHydrationReminderMessage());
builder.write(writeCharacteristic, makeSetLiftWristMessage());
builder.write(writeCharacteristic, Message.encodeSetAlarms(new ArrayList(DBHelper.getAlarms(gbDevice))));
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
return builder;
}
@Override
public void onSendConfiguration(final String config) {
switch (config) {
case DeviceSettingsPreferenceConst.PREF_LANGUAGE:
case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT:
sendMessage("update-language+timeformat", makeSetSystemDataMessage());
return;
case SettingsActivity.PREF_MEASUREMENT_SYSTEM:
sendMessage("update-units", makeSetUnitMessage());
return;
case ActivityUser.PREF_USER_WEIGHT_KG:
case ActivityUser.PREF_USER_GENDER:
case ActivityUser.PREF_USER_HEIGHT_CM:
case ActivityUser.PREF_USER_YEAR_OF_BIRTH:
case DeviceSettingsPreferenceConst.PREF_USER_FITNESS_GOAL:
sendMessage("update-user-info", makeSetUserInfoMessage());
return;
case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_SWITCH:
case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_START:
case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_END:
case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_INTERVAL: {
byte[] msg = makeSetAutoHeartRate();
if (msg != null) {
sendMessage("update-auto-heart-rate", msg);
}
return;
}
case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO:
case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START:
case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END: {
byte[] msg = makeSetDoNotDisturbMessage();
if (msg != null)
sendMessage("update-do-not-disturb", msg);
return;
}
case DeviceSettingsPreferenceConst.PREF_HYDRATION_SWITCH:
case DeviceSettingsPreferenceConst.PREF_HYDRATION_PERIOD: {
byte[] msg = makeSetHydrationReminderMessage();
if (msg != null)
sendMessage("update-hydration-reminder", msg);
return;
}
case DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT:
sendMessage("update-lift-wrist", makeSetLiftWristMessage());
return;
}
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
if (super.onCharacteristicChanged(gatt, characteristic)) {
return true;
}
if (!characteristic.getUuid().equals(HamaFit6900Constants.UUID_CHARACTERISTIC_RX)) {
return false;
}
byte[] receivedData = characteristic.getValue();
Message.CommandMessage cmdMsg = Message.decodeCommandMessage(receivedData);
if (cmdMsg == null) {
return false;
}
final byte[] cmdArgs = cmdMsg.cmdArgs;
switch (cmdMsg.cmd) {
case 1:
switch (cmdMsg.key) {
case 19: // response to GetFirmwareVersion
final String fwVersion = String.format("%d.%d.%d", cmdArgs[0] & 0xFF, cmdArgs[1] & 0xFF, cmdArgs[2] & 0xFF);
// data[3]: bracelet type
// data[>3]: ?
handleFirmwareVersion(fwVersion);
return true;
}
break;
case 2:
switch (cmdMsg.key) {
case 1: // response to SetUnit; no args
case 32: // response to SetDateTime; no args
case 33: // response to SetAlarms; no args
case 35: // response to SetUserInfo; no args
case 39: // response to SetSystemData; no args
case 40: // response to SetHydrationReminder; no args
return true;
}
break;
case 4:
switch (cmdMsg.key) {
case 70: // notification msg: camera - open
handleCameraRemote(GBDeviceEventCameraRemote.Event.OPEN_CAMERA);
return true;
case 71: // notification msg: camera - capture
handleCameraRemote(GBDeviceEventCameraRemote.Event.TAKE_PICTURE);
return true;
case 72: // notification msg: camera - close
handleCameraRemote(GBDeviceEventCameraRemote.Event.CLOSE_CAMERA);
return true;
case 74: // response to SetLiftWriteDisplayOn; no args
return true;
case 65: // response to GetBatteryStatus
handleBatteryStatus(cmdArgs);
return true;
}
break;
case 5:
switch (cmdMsg.key) {
case 80: // response to FindDevice; length=1, [0]
return true;
case 81: // notification msg: find phone; watch sends no notification to stop it
handleFindPhone(true);
return true;
}
break;
case 6:
switch (cmdMsg.key) {
case 96: // response to ShowNotification; no args
case 100: // response to SetDoNotDisturb; no args
return true;
}
break;
case 9:
switch (cmdMsg.key) {
case 146: // response to SetAutoHeartRate; no args
return true;
}
break;
case 10:
switch (cmdMsg.key) {
case 171: // notification msg: heart rate update
// int heartRate = cmdArgs[0] & 0xFF;
break;
case 172: // notification msg: steps update
// int32 steps; float calories [kCal]; float distance [km]
//int steps = Message.decodeInt32(cmdArgs, 0);
break;
}
break;
case 13:
switch (cmdMsg.key) {
case 2: // notification msg: hang up call
// in case of ShowNotification(INCOMING_CALL,..) the watch shows a hang up
// button which triggers this notification. Accepting a call is not supported.
handleCallReject();
return true;
case 4: // notification msg: media player - play
case 5: // notification msg: media player - pause
// Watch only shows a single play/pause button and has no idea of media player playing state.
// So just toggle between play and pause.
handleMusicControl(GBDeviceEventMusicControl.Event.PLAYPAUSE);
return true;
case 6: // notification msg: media player - previous
handleMusicControl(GBDeviceEventMusicControl.Event.PREVIOUS);
return true;
case 7: // notification msg: media player - next
handleMusicControl(GBDeviceEventMusicControl.Event.NEXT);
return true;
}
break;
}
//LOG.debug(" command {},{} | NOT HANDLED", cmdMsg.cmd, cmdMsg.key);
return false;
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
if (!getDevicePrefsNotificationEnabled()) {
return;
}
Message.NotificationType type = Message.NotificationType.UNKNOWN;
switch (notificationSpec.type) {
case FACEBOOK:
case FACEBOOK_MESSENGER:
type = Message.NotificationType.FACEBOOK;
break;
case GENERIC_SMS:
type = Message.NotificationType.SMS;
break;
case INSTAGRAM:
type = Message.NotificationType.INSTAGRAM;
break;
case LINKEDIN:
type = Message.NotificationType.LINKEDIN;
break;
case TWITTER:
type = Message.NotificationType.TWITTER;
break;
case WHATSAPP:
type = Message.NotificationType.WHATSAPP;
break;
}
final String notificationMsg = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title);
final String uniqueTaskName = "notification" + notificationCount;
notificationCount++;
sendMessage(uniqueTaskName, Message.encodeShowNotification(type, notificationMsg));
}
@Override
public void onSetTime() {
sendMessage("set-datetime", makeSetDateTimeMessage());
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
sendMessage("set-alarms", Message.encodeSetAlarms(alarms));
}
@Override
public void onSetCallState(CallSpec callSpec) {
switch (callSpec.command) {
case CallSpec.CALL_INCOMING: {
if (getDevicePrefsNotificationEnabled()) {
final String text = StringUtils.getFirstOf(callSpec.name, callSpec.number);
sendMessage("notification-call-incoming",
Message.encodeShowNotification(Message.NotificationType.INCOMING_CALL, text));
}
break;
}
case CallSpec.CALL_ACCEPT:
// aborts INCOMING_CALL notification
sendMessage("notification-call-accept",
Message.encodeShowNotification(Message.NotificationType.CALL_ACCEPT, ""));
break;
case CallSpec.CALL_REJECT:
case CallSpec.CALL_END:
// aborts INCOMING_CALL notification
sendMessage("notification-call-reject",
Message.encodeShowNotification(Message.NotificationType.CALL_REJECT, ""));
}
}
@Override
public void onFindDevice(boolean start) {
if (start) {
sendMessage("find-device", Message.encodeFindDevice());
}
}
@Override
public boolean useAutoConnect() {
return true;
}
private boolean sendMessage(String taskName, byte[] message) {
try {
TransactionBuilder builder = performInitialized(taskName);
builder.write(writeCharacteristic, message);
builder.queue(getQueue());
} catch (IOException ex) {
LOG.error(taskName, ex);
return false;
}
return true;
}
private boolean getDevicePrefsNotificationEnabled() {
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
return prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false);
}
private Message.TimeFormat getDevicePrefsTimeFormat() {
GBPrefs gbPrefs = new GBPrefs(new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())));
Message.TimeFormat timeFormat = null;
switch (gbPrefs.getTimeFormat()) {
case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_24H:
timeFormat = Message.TimeFormat.Format24H;
break;
case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H:
timeFormat = Message.TimeFormat.Format12H;
break;
}
return timeFormat;
}
private byte[] makeSetDateTimeMessage() {
return Message.encodeSetDateTime(Calendar.getInstance(), getDevicePrefsTimeFormat());
}
private byte[] makeSetSystemDataMessage() {
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
final String localeString = prefs.getString(DeviceSettingsPreferenceConst.PREF_LANGUAGE, DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO);
String language;
String country;
if (localeString.equals(DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO)) {
language = Locale.getDefault().getLanguage();
country = Locale.getDefault().getCountry();
} else {
language = localeString.substring(0, 2);
country = localeString.substring(3, 5);
}
return Message.encodeSetSystemData(language, country, getDevicePrefsTimeFormat());
}
private byte[] makeSetUnitMessage() {
final Prefs prefs = GBApplication.getPrefs();
String unit = prefs.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, "metric");
return Message.encodeSetUnit(unit.equals("metric"));
}
private byte[] makeSetUserInfoMessage() {
final ActivityUser activityUser = new ActivityUser();
return Message.encodeSetUserInfo(
(activityUser.getGender() == ActivityUser.GENDER_MALE) ? Message.Gender.MALE : Message.Gender.FEMALE,
activityUser.getAge(),
activityUser.getHeightCm(),
activityUser.getWeightKg(),
activityUser.getStepsGoal());
}
private byte[] makeSetDoNotDisturbMessage() {
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
final String enabled = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO, "off");
if (!enabled.equals(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_SCHEDULED)) {
return Message.encodeSetDoNotDisturb(false, 0, 0, 0, 0);
}
final String start = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START, "22:00");
final String end = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END, "6:00");
final Calendar startCalendar = GregorianCalendar.getInstance();
final Calendar endCalendar = GregorianCalendar.getInstance();
final DateFormat df = new SimpleDateFormat("HH:mm");
try {
startCalendar.setTime(df.parse(start));
endCalendar.setTime(df.parse(end));
} catch (ParseException e) {
return null;
}
return Message.encodeSetDoNotDisturb(true, startCalendar.get(Calendar.HOUR_OF_DAY), startCalendar.get(Calendar.MINUTE),
endCalendar.get(Calendar.HOUR_OF_DAY), endCalendar.get(Calendar.MINUTE));
}
private byte[] makeSetAutoHeartRate() {
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
final boolean enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_SWITCH, false);
if (!enabled) {
return Message.encodeSetAutoHeartRate(false, 0, 0, 0, 0, 0);
}
// PREF_AUTOHEARTRATE_SLEEP is not supported
final String start = prefs.getString(DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_START, "22:00");
final String end = prefs.getString(DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_END, "6:00");
final String intervalStr = prefs.getString(DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_INTERVAL, "2");
final Calendar startCalendar = GregorianCalendar.getInstance();
final Calendar endCalendar = GregorianCalendar.getInstance();
final DateFormat df = new SimpleDateFormat("HH:mm");
final int intervalMinutes;
try {
startCalendar.setTime(df.parse(start));
endCalendar.setTime(df.parse(end));
} catch (ParseException e) {
return null;
}
try {
intervalMinutes = Integer.parseInt(intervalStr);
} catch (NumberFormatException e) {
return null;
}
return Message.encodeSetAutoHeartRate(true, startCalendar.get(Calendar.HOUR_OF_DAY), startCalendar.get(Calendar.MINUTE),
endCalendar.get(Calendar.HOUR_OF_DAY), endCalendar.get(Calendar.MINUTE), intervalMinutes);
}
private byte[] makeSetHydrationReminderMessage() {
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
boolean enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYDRATION_SWITCH, false);
final String intervalStr = prefs.getString(DeviceSettingsPreferenceConst.PREF_HYDRATION_PERIOD, "60");
int intervalMinutes;
try {
intervalMinutes = Integer.parseInt(intervalStr);
} catch (NumberFormatException e) {
return null;
}
if (intervalMinutes == 0) {
enabled = false;
}
// Drink reminder notifications honor the do-not-disturb time range.
// Start time must be > then end time; e.g. start=19, end=1 won't work.
// enable=true and start=end=0 behaves as disabled.
int startHour = 0;
int startMin = 0;
int endHour = 23;
int endMin = 59;
return Message.encodeSetHydrationReminder(enabled, startHour, startMin, endHour, endMin, intervalMinutes);
}
private byte[] makeSetLiftWristMessage() {
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
final String enabled = prefs.getString(DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT, "off");
final boolean isEnabled = !enabled.equals("off");
return Message.encodeSetLiftWristDisplayOn(isEnabled);
}
private void handleFirmwareVersion(String fwVersion) {
final GBDeviceEventVersionInfo event = new GBDeviceEventVersionInfo();
event.fwVersion = fwVersion;
event.fwVersion2 = null;
event.hwVersion = null;
evaluateGBDeviceEvent(event);
}
private void handleCallReject() {
final GBDeviceEventCallControl event = new GBDeviceEventCallControl();
event.event = GBDeviceEventCallControl.Event.REJECT;
evaluateGBDeviceEvent(event);
}
private void handleCameraRemote(GBDeviceEventCameraRemote.Event eventType) {
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
if (!prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE, false))
return;
final GBDeviceEventCameraRemote event = new GBDeviceEventCameraRemote();
event.event = eventType;
evaluateGBDeviceEvent(event);
}
private void handleMusicControl(GBDeviceEventMusicControl.Event eventType) {
final GBDeviceEventMusicControl event = new GBDeviceEventMusicControl();
event.event = eventType;
evaluateGBDeviceEvent(event);
}
private void handleFindPhone(boolean start) {
if (start) {
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
String findPhone = sharedPreferences.getString(PREF_FIND_PHONE, getContext().getString(R.string.p_off));
if (findPhone.equals("off"))
return;
String durationSecStr = sharedPreferences.getString(PREF_FIND_PHONE_DURATION, "");
int durationSec;
try {
durationSec = Integer.parseInt(durationSecStr);
} catch (Exception ex) {
durationSec = 60;
}
if (durationSec <= 0)
return;
GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
evaluateGBDeviceEvent(findPhoneEvent);
try {
this.findPhoneStopNotificationHandler.postDelayed(new Runnable() {
@Override
public void run() {
handleFindPhone(false);
}
}, durationSec * 1000);
} catch (Exception ex) {
handleFindPhone(false);
}
} else {
GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
evaluateGBDeviceEvent(findPhoneEvent);
}
}
private void handleBatteryStatus(byte[] cmdArgs) {
/*
final int level = cmdArgs[0] & 0xFF;
final int type = cmdArgs[1] & 0xFF;
Disabled since watch always returns a level value of 60
Also missing: status needs to be polled regularly
GBDeviceEventBatteryInfo event = new GBDeviceEventBatteryInfo();
event.level = level;
evaluateGBDeviceEvent(event); */
}
}

View File

@ -1618,6 +1618,7 @@
<string name="devicetype_sg2">Lemfo SG2</string>
<string name="devicetype_lefun">Lefun</string>
<string name="devicetype_bohemic_smart_bracelet">Bohemic Smart Bracelet</string>
<string name="devicetype_hama_fit6900">Hama Fit6900</string>
<!-- Menus on the smart device -->
<string name="menuitem_nothing">Nothing</string>
<string name="menuitem_status">Status</string>