/* Copyright (C) 2019-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniel Dakhno
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.qhybrid.adapter.misfit;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_ACTIVITY_POINT;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_STEP_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_STEP_GOAL;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_VIBRATION_STRENGTH;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_BUTTON_PRESS;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_FILE_UPLOADED;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.net.Uri;
import android.util.SparseArray;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.TimeZone;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ActivityPointGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.AnimationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.BatteryLevelRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.DownloadFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.EraseFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.FileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetCountdownSettingsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetCurrentStepCountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetStepGoalRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetVibrationStrengthRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GoalTrackingGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ListFilesRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.MoveHandsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.OTAEnterRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.OTAEraseRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ReleaseHandsControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.RequestHandControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SaveCalibrationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetCurrentStepCountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetStepGoalRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetTimeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetVibrationStrengthRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.UploadFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.VibrateRequest;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class MisfitWatchAdapter extends WatchAdapter {
private int lastButtonIndex = -1;
private final SparseArray responseFilters = new SparseArray<>();
private UploadFileRequest uploadFileRequest;
private Request fileRequest = null;
private Queue requestQueue = new ArrayDeque<>();
private Logger logger = LoggerFactory.getLogger(getClass());
public MisfitWatchAdapter(QHybridSupport deviceSupport) {
super(deviceSupport);
fillResponseList();
}
@Override
public void initialize() {
requestQueue.add(new GetStepGoalRequest());
requestQueue.add(new GetVibrationStrengthRequest());
requestQueue.add(new ActivityPointGetRequest());
requestQueue.add(prepareSetTimeRequest());
requestQueue.add(new AnimationRequest());
requestQueue.add(new SetCurrentStepCountRequest((int) (999999 * getDeviceSupport().calculateNotificationProgress())));
queueWrite(new GetCurrentStepCountRequest());
getDeviceSupport().getDevice().setState(GBDevice.State.INITIALIZED);
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
}
private SetTimeRequest prepareSetTimeRequest() {
long millis = System.currentTimeMillis();
TimeZone zone = new GregorianCalendar().getTimeZone();
return new SetTimeRequest(
(int) (millis / 1000 + getDeviceSupport().getTimeOffset() * 60),
(short) (millis % 1000),
(short) ((zone.getRawOffset() + zone.getDSTSavings()) / 60000));
}
@Override
public void playPairingAnimation() {
queueWrite(new AnimationRequest());
}
@Override
public void playNotification(NotificationConfiguration config) {
queueWrite(new PlayNotificationRequest(
config.getVibration(),
config.getHour(),
config.getMin(),
config.getSubEye()
));
}
@Override
public void setTime() {
queueWrite(prepareSetTimeRequest());
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
GBDevice gbDevice = getDeviceSupport().getDevice();
switch (characteristic.getUuid().toString()) {
case "3dda0004-957f-7d4a-34a6-74696673696d":
case "3dda0003-957f-7d4a-34a6-74696673696d": {
return handleFileDownloadCharacteristic(characteristic);
}
case "3dda0007-957f-7d4a-34a6-74696673696d": {
return handleFileUploadCharacteristic(characteristic);
}
case "3dda0002-957f-7d4a-34a6-74696673696d": {
return handleBasicCharacteristic(characteristic);
}
case "3dda0006-957f-7d4a-34a6-74696673696d": {
return handleButtonCharacteristic(characteristic);
}
case "00002a19-0000-1000-8000-00805f9b34fb": {
short level = characteristic.getValue()[0];
gbDevice.setBatteryLevel(level);
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
batteryInfo.level = gbDevice.getBatteryLevel();
batteryInfo.state = BatteryState.BATTERY_NORMAL;
getDeviceSupport().handleGBDeviceEvent(batteryInfo);
break;
}
default: {
log("unknown shit on " + characteristic.getUuid().toString() + ": " + arrayToString(characteristic.getValue()));
try {
File charLog = FileUtils.getExternalFile("qFiles/charLog.txt");
try (FileOutputStream fos = new FileOutputStream(charLog, true)) {
fos.write((new Date().toString() + ": " + characteristic.getUuid().toString() + ": " + arrayToString(characteristic.getValue())).getBytes());
}
} catch (IOException e) {
GB.log("error", GB.ERROR, e);
}
break;
}
}
return getDeviceSupport().onCharacteristicChanged(gatt, characteristic);
}
private void fillResponseList() {
Class extends Request>[] classes = new Class[]{
BatteryLevelRequest.class,
GetStepGoalRequest.class,
GetVibrationStrengthRequest.class,
GetCurrentStepCountRequest.class,
OTAEnterRequest.class,
GoalTrackingGetRequest.class,
ActivityPointGetRequest.class,
GetCountdownSettingsRequest.class
};
for (Class extends Request> c : classes) {
try {
c.getSuperclass().getDeclaredMethod("handleResponse", BluetoothGattCharacteristic.class);
Request object = c.newInstance();
byte[] sequence = object.getStartSequence();
if (sequence.length > 1) {
responseFilters.put((int) object.getStartSequence()[1], object);
log("response filter " + object.getStartSequence()[1] + ": " + c.getSimpleName());
}
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException e) {
log("skipping class " + c.getName());
}
}
}
private boolean handleBasicCharacteristic(BluetoothGattCharacteristic characteristic) {
byte[] values = characteristic.getValue();
Request request = resolveAnswer(characteristic);
GBDevice gbDevice = getDeviceSupport().getDevice();
if (request == null) {
StringBuilder valueString = new StringBuilder(String.valueOf(values[0]));
for (int i = 1; i < characteristic.getValue().length; i++) {
valueString.append(", ").append(values[i]);
}
log("unable to resolve " + characteristic.getUuid().toString() + ": " + valueString);
return true;
}
log("response: " + request.getClass().getSimpleName());
request.handleResponse(characteristic);
if (request instanceof GetStepGoalRequest) {
gbDevice.addDeviceInfo(new GenericItem(ITEM_STEP_GOAL, String.valueOf(((GetStepGoalRequest) request).stepGoal)));
} else if (request instanceof GetVibrationStrengthRequest) {
int strength = ((GetVibrationStrengthRequest) request).strength;
gbDevice.addDeviceInfo(new GenericItem(ITEM_VIBRATION_STRENGTH, String.valueOf(strength)));
} else if (request instanceof GetCurrentStepCountRequest) {
int steps = ((GetCurrentStepCountRequest) request).steps;
logger.debug("get current steps: " + steps);
try {
File file = FileUtils.getExternalFile("qFiles/steps");
logger.debug("Writing file " + file.getPath());
try (FileOutputStream fos = new FileOutputStream(file, true)) {
fos.write((System.currentTimeMillis() + ": " + steps + "\n").getBytes());
}
logger.debug("file written.");
} catch (Exception e) {
GB.log("error", GB.ERROR, e);
}
gbDevice.addDeviceInfo(new GenericItem(ITEM_STEP_COUNT, String.valueOf(((GetCurrentStepCountRequest) request).steps)));
} else if (request instanceof OTAEnterRequest) {
if (((OTAEnterRequest) request).success) {
fileRequest = new OTAEraseRequest(1024 << 16);
queueWrite(fileRequest);
}
} else if (request instanceof ActivityPointGetRequest) {
gbDevice.addDeviceInfo(new GenericItem(ITEM_ACTIVITY_POINT, String.valueOf(((ActivityPointGetRequest) request).activityPoint)));
}
try {
queueWrite(requestQueue.remove());
} catch (NoSuchElementException e) {
}
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(DeviceManager.ACTION_DEVICES_CHANGED));
return true;
}
private Request resolveAnswer(BluetoothGattCharacteristic characteristic) {
byte[] values = characteristic.getValue();
if (values[0] != 3) return null;
return responseFilters.get(values[1]);
}
private boolean handleFileDownloadCharacteristic(BluetoothGattCharacteristic characteristic) {
Request request;
request = fileRequest;
request.handleResponse(characteristic);
if (request instanceof ListFilesRequest) {
if (((ListFilesRequest) request).completed) {
logger.debug("File count: " + ((ListFilesRequest) request).fileCount + " size: " + ((ListFilesRequest) request).size);
if (((ListFilesRequest) request).fileCount == 0) return true;
// queueWrite(new DownloadFileRequest((short) (256 + ((ListFilesRequest) request).fileCount)));
}
} else if (request instanceof DownloadFileRequest) {
if (((FileRequest) request).completed) {
logger.debug("file " + ((DownloadFileRequest) request).fileHandle + " completed: " + ((DownloadFileRequest) request).size);
// backupFile((DownloadFileRequest) request);
}
} else if (request instanceof EraseFileRequest) {
if (((EraseFileRequest) request).fileHandle > 257) {
queueWrite(new DownloadFileRequest((short) (((EraseFileRequest) request).fileHandle - 1)));
}
}
return true;
}
private boolean handleFileUploadCharacteristic(BluetoothGattCharacteristic characteristic) {
if (uploadFileRequest == null) {
logger.debug("no uploadFileRequest to handle response");
return true;
}
uploadFileRequest.handleResponse(characteristic);
switch (uploadFileRequest.state) {
case ERROR:
Intent fileIntent = new Intent(QHYBRID_EVENT_FILE_UPLOADED);
fileIntent.putExtra("EXTRA_ERROR", true);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(fileIntent);
uploadFileRequest = null;
break;
case UPLOAD:
for (byte[] packet : this.uploadFileRequest.packets) {
new TransactionBuilder("File upload").write(characteristic, packet).queue(getDeviceSupport().getQueue());
}
break;
case UPLOADED:
fileIntent = new Intent(QHYBRID_EVENT_FILE_UPLOADED);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(fileIntent);
uploadFileRequest = null;
break;
}
return true;
}
private boolean handleButtonCharacteristic(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
if (value.length != 11) {
logger.debug("wrong button message");
return true;
}
int index = value[6] & 0xFF;
int button = value[8] >> 4 & 0xFF;
if (index != this.lastButtonIndex) {
lastButtonIndex = index;
logger.debug("Button press on button " + button);
Intent i = new Intent(QHYBRID_EVENT_BUTTON_PRESS);
i.setPackage(BuildConfig.APPLICATION_ID);
i.putExtra("BUTTON", button);
//ByteBuffer buffer = ByteBuffer.allocate(16);
//buffer.put(new byte[]{0x01, 0x00, 0x08});
//buffer.put(value, 2, 8);
//buffer.put(new byte[]{(byte)0xFF, 0x05, 0x00, 0x01, 0x00});
//FilePutRequest request = new FilePutRequest((short)0, buffer.array());
//for(byte[] packet : request.packets){
// new TransactionBuilder("File upload").write(getCharacteristic(UUID.fromString("3dda0007-957f-7d4a-34a6-74696673696d")), packet).queue(getQueue());
//}
getContext().sendBroadcast(i);
}
return true;
}
private void log(String message){
logger.debug(message);
}
public void setActivityHand(double progress) {
queueWrite(new SetCurrentStepCountRequest(Math.min((int) (1000000 * progress), 999999)));
}
public void vibrate(PlayNotificationRequest.VibrationType vibration) {
queueWrite(new PlayNotificationRequest(vibration, -1, -1));
}
@Override
public void vibrateFindMyDevicePattern() {
queueWrite(new VibrateRequest(false, (short) 4, (short) 1));
}
@Override
public void requestHandsControl() {
queueWrite(new RequestHandControlRequest());
}
@Override
public void releaseHandsControl() {
queueWrite(new ReleaseHandsControlRequest());
}
@Override
public void setHands(MoveHandsRequest.MovementConfiguration movement) {
queueWrite(new MoveHandsRequest(movement));
}
@Override
public void saveCalibration() {
queueWrite(new SaveCalibrationRequest());
}
@Override
public void setStepGoal(int stepGoal) {
queueWrite(new SetStepGoalRequest(stepGoal));
}
@Override
public void setVibrationStrength(short strength) {
queueWrite(new SetVibrationStrengthRequest(strength));
}
@Override
public void syncNotificationSettings() {
}
@Override
public void onTestNewFunction() {
}
@Override
public void setTimezoneOffsetMinutes(short offset) {
GB.toast("old firmware does't support timezones", Toast.LENGTH_LONG, GB.ERROR);
}
@Override
public void onInstallApp(Uri uri) {
}
@Override
public boolean supportsFindDevice() {
return supportsExtendedVibration();
}
@Override
public boolean supportsExtendedVibration() {
String modelNumber = getDeviceSupport().getDevice().getModel();
switch (modelNumber) {
case "HW.0.0":
return true;
case "HL.0.0":
return false;
case "DN.1.0":
return true;
}
throw new UnsupportedOperationException("Model " + modelNumber + " not supported");
}
@Override
public boolean supportsActivityHand() {
String modelNumber = getDeviceSupport().getDevice().getModel();
switch (modelNumber) {
case "HW.0.0":
return true;
case "HL.0.0":
return false;
case "DN.1.0":
return false;
}
throw new UnsupportedOperationException("Model " + modelNumber + " not supported");
}
@Override
public void onFetchActivityData() {
requestQueue.add(new BatteryLevelRequest());
requestQueue.add(new GetCurrentStepCountRequest());
// requestQueue.add(new ListFilesRequest());
queueWrite(new ActivityPointGetRequest());
getDeviceSupport().getDevice().unsetBusyTask();
GB.updateTransferNotification(null, "", false, 100, getContext());
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
GB.signalActivityDataFinish(getDeviceSupport().getDevice());
}
@Override
public void onSetAlarms(ArrayList extends Alarm> alarms) {
GB.toast("alarms not supported with this firmware", Toast.LENGTH_LONG, GB.ERROR);
return;
}
@Override
public void onSendConfiguration(String config) {
}
@Override
public void overwriteButtons(String jsonConfigString) {
uploadFileRequest = new UploadFileRequest((short) 0x0800, new byte[]{
(byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x10, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00, (byte) 0x00, (byte) 0x20, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00, (byte) 0x00,
(byte) 0x30, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x2E, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
(byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x0F, (byte) 0x00, (byte) 0x8B, (byte) 0x00, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x01,
(byte) 0x08, (byte) 0x01, (byte) 0x14, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0xBF, (byte) 0xD5, (byte) 0x54, (byte) 0xD1,
(byte) 0x00
});
queueWrite(uploadFileRequest);
}
private void queueWrite(Request request) {
new TransactionBuilder(request.getClass().getSimpleName()).write(getDeviceSupport().getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getDeviceSupport().getQueue());
// if (request instanceof FileRequest) this.fileRequest = request;
if (!request.expectsResponse()) {
try {
queueWrite(requestQueue.remove());
} catch (NoSuchElementException e) {
}
}
}
}