/* Copyright (C) 2023-2024 Andreas Böhler, Johannes Krude
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.casio.gbx100;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.Casio2C2DSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gbx100.CasioGBX100DeviceSupport;
public class InitOperation extends AbstractBTLEOperation {
private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class);
private final TransactionBuilder builder;
private final CasioGBX100DeviceSupport support;
private final boolean mFirstConnect;
private final boolean mNeedsGetConfiguration;
private boolean mWriteAllFeaturesInitPending = false;
public InitOperation(CasioGBX100DeviceSupport support, TransactionBuilder builder, boolean firstConnect, boolean needsGetConfiguration) {
super(support);
this.builder = builder;
this.support = support;
this.mFirstConnect = firstConnect;
this.mNeedsGetConfiguration = needsGetConfiguration;
LOG.info("mFirstConnect: " + mFirstConnect);
builder.setCallback(this);
}
private void writeAllFeaturesRequest(byte[] arr) {
try {
TransactionBuilder builder = createTransactionBuilder("writeAllFeaturesRequest");
builder.setCallback(this);
support.writeAllFeaturesRequest(builder, arr);
support.performImmediately(builder);
} catch(IOException e) {
LOG.error("Error writing all features: " + e.getMessage());
}
}
private void writeAllFeatures(byte[] arr) {
try {
TransactionBuilder builder = createTransactionBuilder("writeAllFeatures");
builder.setCallback(this);
support.writeAllFeatures(builder, arr);
support.performImmediately(builder);
} catch(IOException e) {
LOG.error("Error writing all features: " + e.getMessage());
}
}
private void writeAllFeaturesInit(TransactionBuilder builder) {
byte[] arr = new byte[2];
arr[0] = 0x00;
arr[1] = 0x01;
mWriteAllFeaturesInitPending = true;
if(builder != null)
support.writeAllFeatures(builder, arr);
else
writeAllFeatures(arr);
}
private void writeAllFeaturesInit() {
writeAllFeaturesInit(null);
}
private void requestWatchName(TransactionBuilder builder) {
support.writeAllFeaturesRequest(builder, new byte[]{Casio2C2DSupport.FEATURE_WATCH_NAME});
}
private void requestWatchName() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_WATCH_NAME});
}
private void requestBleConfiguration() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_BLE_FEATURES});
}
private void requestBleSettings() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_SETTING_FOR_BLE});
}
private void requestAdvertisingParameters() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_ADVERTISE_PARAMETER_MANAGER});
}
private void requestConnectionParameters() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_CONNECTION_PARAMETER_MANAGER});
}
private void requestModuleId() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_MODULE_ID});
}
private void requestWatchCondition() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_WATCH_CONDITION});
}
private void requestVersionInformation() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_VERSION_INFORMATION});
}
private void requestDstWatchState() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_DST_WATCH_STATE});
}
private void requestDstSetting() {
writeAllFeaturesRequest(new byte[]{Casio2C2DSupport.FEATURE_DST_SETTING});
}
private void writeAppInformation() {
// FIXME: The App ID should be auto-generated and stored in the
// preferences instead of hard-coding it here
// CASIO_APP_INFORMATION:
byte arr[] = new byte[12];
arr[0] = Casio2C2DSupport.FEATURE_APP_INFORMATION;
for(int i=0; i<10; i++)
arr[i+1] = (byte) (i & 0xff);
arr[11] = 2;
writeAllFeatures(arr);
}
private void writeBleSettings(byte[] data) {
if(data.length < 14)
return;
// FIXME: We use hard-coded values here, should they be changeable?
// CASIO_SETTING_FOR_BLE:
// Don't forget first byte, we write to ALL_FEATURES!
// 0000000600000000000100801e00
// rest: mirror
// mBleSettings[5] = (byte)(connInterval & 0xff);
// mBleSettings[6] = (byte)((connInterval >> 8) & 0xff);
// mBleSettings[7] = (byte)(slaveLatency & 0xff);
// mBleSettings[8] = (byte)((slaveLatency >> 8) & 0xff);
// mBleSettings[11] = Auto Reconnect Hour
// mBleSettings[12] = Auto Reconnect Minute
// Shift-by-one b/c of ALL_FEATURES!
data[12] = (byte)0x80;
data[13] = (byte)0x1e;
writeAllFeatures(data);
}
private void writeAdvertisingParameters() {
// FIXME: This is hardcoded for now, based on the HCI log of my watch
// CASIO_ADVERTISE_PARAMETER_MANAGER
// Don't forget first byte, we write to ALL_FEATURES!
// 50000a4001050540060a02a000054001051c00
// byte arr[] = new byte[19];
// arr[0] = advertiseParamAfterSleepResetOrKeyFunctionAdvertiseInterval & 0xff;
// arr[1] = advertiseParamAfterSleepResetOrKeyFunctionAdvertiseInterval >> 8 & 0xff;
// arr[2] = getAdvertiseParamAfterSleepResetOrKeyFunctionAdvertisingTime;
// arr[3] = advertiseParamHighIntervalAdvertisementAfterLinkLossAdvertiseInterval & 0xff;
// arr[4] = advertiseParamHighIntervalAdvertisementAfterLinkLossAdvertiseInterval >> 8 & 0xff;
// arr[5] = getAdvertiseParamHighIntervalAdvertisementAfterLinkLossAdvertisingTime;
// arr[6] = getAdvertiseParamHighIntervalAdvertisementAfterLinkLossFrequencyToAdvertise;
// arr[7] = advertiseParamLowIntervalAdvertisementAfterLinkLossAdvertiseInterval & 0xff;
// arr[8] = advertiseParamLowIntervalAdvertisementAfterLinkLossAdvertiseInterval >> 8 & 0xff;
// arr[9] = getAdvertiseParamLowIntervalAdvertisementAfterLinkLossAdvertisingTime & 0xff;
// arr[10] = getAdvertiseParamLowIntervalAdvertisementAfterLinkLossFrequencyToAdvertise & 0xff;
// arr[11] = advertiseParamRegularBeaconAdvertisementAdvertiseInterval & 0xff;
// arr[12] = advertiseParamRegularBeaconAdvertisementAdvertiseInterval >> 8 & 0xff;
// arr[13] = getAdvertiseParamRegularBeaconAdvertisementAdvertisingTime;
// arr[14] = advertiseParamAdvertiseImmediatelyAfterLinkLossAdvertiseInterval & 0xff;
// arr[15] = advertiseParamAdvertiseImmediatelyAfterLinkLossAdvertiseInterval >> 8 & 0xff;
// arr[16] = getAdvertiseParamAdvertiseImmediatelyAfterLinkLossAdvertisingTime
// arr[17] = advertiseParamPeriodUntilSuspensionOfAutoAdvertise & 0xff;
// arr[18] = advertiseParamPeriodUntilSuspensionOfAutoAdvertise >> 8 & 0xff;
byte arr[] = new byte[20];
arr[0] = Casio2C2DSupport.FEATURE_ADVERTISE_PARAMETER_MANAGER;
arr[1] = (byte)0x50;
arr[2] = (byte)0x00;
arr[3] = (byte)0x0a;
arr[4] = (byte)0x40;
arr[5] = (byte)0x01;
arr[6] = (byte)0x05;
arr[7] = (byte)0x05;
arr[8] = (byte)0x40;
arr[9] = (byte)0x06;
arr[10] = (byte)0x0a;
arr[11] = (byte)0x02;
arr[12] = (byte)0xa0;
arr[13] = (byte)0x00;
arr[14] = (byte)0x05;
arr[15] = (byte)0x40;
arr[16] = (byte)0x01;
arr[17] = (byte)0x05;
arr[18] = (byte)0x1c;
arr[19] = (byte)0x00;
writeAllFeatures(arr);
}
private void writeConnectionParameters() {
// FIXME: This is hardcoded for now, based on the HCI log of my watch
// CASIO_CONNECTION_PARAMETER_MANAGER
// Don't forget first byte, we write to ALL_FEATURES!
// 00340140010400dc05
// byte[] arr = new byte[9];
// arr[0] = getConnectionParamCommand;
// arr[1] = connectionParamMinimumConnectionInterval & 0xff;
// arr[2] = connectionParamMinimumConnectionInterval >> 8 & 0xff;
// arr[3] = connectionParamMaxConnectionInterval & 0xff;
// arr[4] = connectionParamMaxConnectionInterval >> 8 & 0xff;
// arr[5] = connectionParamConnLatency & 0xff;
// arr[6] = connectionParamConnLatency >> 8 & 0xff;
// arr[7] = connectionParamSupervisionTimeout & 0xff;
// arr[8] = connectionParamSupervisionTimeout >> 8 & 0xff;
byte[] arr = new byte[10];
arr[0] = Casio2C2DSupport.FEATURE_CONNECTION_PARAMETER_MANAGER;
arr[1] = (byte)0x00;
arr[2] = (byte)0x34;
arr[3] = (byte)0x01;
arr[4] = (byte)0x40;
arr[5] = (byte)0x01;
arr[6] = (byte)0x04;
arr[7] = (byte)0x00;
arr[8] = (byte)0xdc;
arr[9] = (byte)0x05;
writeAllFeatures(arr);
}
private void enableAllFeatures(boolean enable) {
try {
TransactionBuilder builder = createTransactionBuilder("notifyAllFeatures");
builder.setCallback(this);
enableAllFeatures(builder, enable);
support.performImmediately(builder);
} catch(IOException e) {
LOG.error("Error setting notification value on all features: " + e.getMessage());
}
}
private void enableAllFeatures(TransactionBuilder builder, boolean enable) {
builder.notify(getCharacteristic(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID), enable);
}
@Override
protected void doPerform() throws IOException {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
enableAllFeatures(builder, true);
requestWatchName(builder);
}
@Override
public TransactionBuilder performInitialized(String taskName) throws IOException {
throw new UnsupportedOperationException("This IS the initialization class, you cannot call this method");
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
UUID characteristicUUID = characteristic.getUuid();
byte[] data = characteristic.getValue();
if(data.length == 0)
return true;
// Abort a stalled allFeaturesInit write
if(mWriteAllFeaturesInitPending) {
getQueue().abortCurrentTransaction();
mWriteAllFeaturesInitPending = false;
}
if(characteristicUUID.equals(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID)) {
if(data[0] == Casio2C2DSupport.FEATURE_WATCH_NAME) {
LOG.debug("Got watch name, requesting BLE features; should write CASIO_APP_INFORMATION");
writeAppInformation();
// The rest of the init sequence is not strictly needed; we keep
// it here for future reference.
if(mFirstConnect)
requestBleConfiguration();
} else if(data[0] == Casio2C2DSupport.FEATURE_BLE_FEATURES) {
LOG.debug("Got BLE features, requesting BLE settings");
requestBleSettings();
} else if(data[0] == Casio2C2DSupport.FEATURE_SETTING_FOR_BLE) {
LOG.debug("Got BLE settings, requesting advertisement parameters; should write BLE settings");
writeBleSettings(data);
requestAdvertisingParameters();
} else if(data[0] == Casio2C2DSupport.FEATURE_ADVERTISE_PARAMETER_MANAGER) {
LOG.debug("Got advertisement parameters, requesting connection parameters; should write advertisement parameters");
writeAdvertisingParameters();
requestConnectionParameters();
} else if(data[0] == Casio2C2DSupport.FEATURE_CONNECTION_PARAMETER_MANAGER) {
LOG.debug("Got connection parameters, requesting module ID; should write connection parameters");
writeConnectionParameters();
requestModuleId();
} else if(data[0] == Casio2C2DSupport.FEATURE_MODULE_ID) {
LOG.debug("Got module ID, requesting watch condition");
requestWatchCondition();
} else if(data[0] == Casio2C2DSupport.FEATURE_WATCH_CONDITION) {
LOG.debug("Got watch condition, requesting version information");
requestVersionInformation();
} else if(data[0] == Casio2C2DSupport.FEATURE_VERSION_INFORMATION) {
LOG.debug("Got version information, requesting DST watch state");
requestDstWatchState();
} else if(data[0] == Casio2C2DSupport.FEATURE_DST_WATCH_STATE) {
LOG.debug("Got DST watch state, requesting DST setting; should write DST watch state");
requestDstSetting();
} else if(data[0] == Casio2C2DSupport.FEATURE_DST_SETTING) {
LOG.debug("Got DST setting, waiting...; should write DST setting and location and radio information");
} else if(data[0] == Casio2C2DSupport.FEATURE_SERVICE_DISCOVERY_MANAGER) {
if(data[1] == 0x02) {
// The writeAllFeaturesInit request triggers bonding. However, the transaction
// never completes. Instead, the watch sends 0x4701 notification and we abort
// the current transaction.
// This is actually the request for the link loss service - it's only received
// on first connection, later reconnects receive 0x47 0x01.
// We also need to send the current time, if not, the watch says "Failed".
LOG.debug("We need to bond here. This is actually the request for the link loss service.");
try {
TransactionBuilder builder = createTransactionBuilder("writeCurrentTime");
support.writeCurrentTime(builder, ZonedDateTime.now());
writeAllFeaturesInit(builder);
support.performImmediately(builder);
} catch(IOException e) {
LOG.error("Error setting device to initialized: " + e.getMessage());
}
} else if(data[1] == 0x01) {
support.setInitialized();
}
} else if(data[0] == 0x3d) {
LOG.info("Init operation done.");
// Finally, we set the state to initialized here!
// If it's the first connection attempt, disconnect the watch and
// reconnect delayed. This is required on Android 13 since it's no longer
// possible to cancel a dangling write request.
// Upon first reconnection, a "getConfiguration" request is required. On further
// reconnections, we just sync the profile.
support.setInitialized();
if(mFirstConnect) {
support.disconnect();
support.reconnectDelayed();
} else if(mNeedsGetConfiguration) {
support.onReadConfiguration(null);
} else {
// on first connect, this is called by onReadConfiguration
support.syncProfile();
}
}
return true;
} else {
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
return super.onCharacteristicChanged(gatt, characteristic);
}
}
@Override
public boolean onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
return super.onCharacteristicRead(gatt, characteristic, status);
}
}