diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java
index 07f10e194..ffd2be395 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java
@@ -1,6 +1,6 @@
/* Copyright (C) 2015-2024 Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Damien Gaignon, Daniel Dakhno, Daniele Gobbetti, Frank Ertl,
- José Rebelo
+ José Rebelo, Johannes Krude
This file is part of Gadgetbridge.
@@ -30,6 +30,7 @@ import androidx.annotation.RequiresApi;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.BondAction;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.FunctionAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.NotifyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.RequestConnectionPriorityAction;
@@ -118,6 +119,11 @@ public class TransactionBuilder {
return add(action);
}
+ // Runs the given function/lambda
+ public TransactionBuilder run(FunctionAction.Function function) {
+ return add(new FunctionAction(function));
+ }
+
public TransactionBuilder add(BtLEAction action) {
mTransaction.add(action);
return this;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/FunctionAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/FunctionAction.java
new file mode 100644
index 000000000..404c64985
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/FunctionAction.java
@@ -0,0 +1,47 @@
+/* Copyright (C) 2023 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.btle.actions;
+
+import android.bluetooth.BluetoothGatt;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.PlainAction;
+
+/**
+ * Invokes the given function
+ */
+public class FunctionAction extends PlainAction {
+
+ public interface Function {
+ public void apply(BluetoothGatt gatt);
+ }
+ private Function function;
+
+ public FunctionAction(Function function) {
+ this.function = function;
+ }
+
+ @Override
+ public boolean run(BluetoothGatt gatt) {
+ function.apply(gatt);
+ return true;
+ }
+
+ @Override
+ public boolean expectsResult() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/Casio2C2DSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/Casio2C2DSupport.java
index 29ad65065..db0e2aa97 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/Casio2C2DSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/Casio2C2DSupport.java
@@ -17,11 +17,22 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio;
import java.time.ZonedDateTime;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+
import java.util.UUID;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
@@ -52,12 +63,24 @@ public abstract class Casio2C2DSupport extends CasioSupport {
public static final byte FEATURE_SETTING_FOR_USER_PROFILE = 0x45;
public static final byte FEATURE_SERVICE_DISCOVERY_MANAGER = 0x47;
+ private static Logger LOG;
+ LinkedList requests = new LinkedList<>();
public Casio2C2DSupport(Logger logger) {
super(logger);
+ LOG = logger;
+ }
+
+ @Override
+ public boolean connect() {
+ requests.clear();
+ return super.connect();
}
public void writeAllFeatures(TransactionBuilder builder, byte[] arr) {
+ if (!requests.isEmpty()) {
+ LOG.warn("writing while waiting for a response may lead to incorrect received responses");
+ }
builder.write(getCharacteristic(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID), arr);
}
@@ -65,6 +88,123 @@ public abstract class Casio2C2DSupport extends CasioSupport {
builder.write(getCharacteristic(CasioConstants.CASIO_READ_REQUEST_FOR_ALL_FEATURES_CHARACTERISTIC_UUID), arr);
}
+ public interface ResponseHandler {
+ void handle(byte[] response);
+ }
+
+ public interface ResponsesHandler {
+ void handle(Map responses);
+ }
+
+ public static class FeatureRequest {
+ byte data[];
+
+ public FeatureRequest(byte arg0) {
+ data = new byte[] {arg0};
+ }
+
+ public FeatureRequest(byte arg0, byte arg1) {
+ data = new byte[] {arg0, arg1};
+ }
+
+ public byte[] getData() {
+ return data.clone();
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(data);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this)
+ return true;
+ if (!(o instanceof FeatureRequest))
+ return false;
+
+ FeatureRequest fr = (FeatureRequest) o;
+ return Arrays.equals(data, fr.data);
+ }
+
+ public boolean matches(byte[] response) {
+ if (response.length > 2 && response[0] == 0xFF && response[1] == 0x81) {
+ if (data.length < response.length - 2)
+ return false;
+ for (int i = 2; i < response.length; i++) {
+ if (response[i] != data[i-2])
+ return false;
+ }
+ return true;
+ } else {
+ if (response.length < data.length)
+ return false;
+ for (int i = 0; i < data.length; i++) {
+ if (response[i] != data[i])
+ return false;
+ }
+ return true;
+ }
+ }
+ }
+
+ private static class RequestWithHandler {
+ public FeatureRequest request;
+ public ResponseHandler handler;
+
+ public RequestWithHandler(FeatureRequest request, ResponseHandler handler) {
+ this.request = request;
+ this.handler = handler;
+ }
+ }
+
+ public void requestFeature(TransactionBuilder builder, FeatureRequest request, ResponseHandler handler) {
+ builder.notify(getCharacteristic(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID), true);
+ writeAllFeaturesRequest(builder, request.getData());
+ builder.run((gatt) -> requests.add(new RequestWithHandler(request, handler)));
+ }
+
+ public void requestFeatures(TransactionBuilder builder, Set requests, ResponsesHandler handler) {
+ HashMap responses = new HashMap();
+
+ HashSet missing = new HashSet();
+ for (FeatureRequest request: requests) {
+ missing.add(request);
+ }
+
+ for (FeatureRequest request: requests) {
+ requestFeature(builder, request, data -> {
+ responses.put(request, data);
+ missing.remove(request);
+ if (missing.isEmpty()) {
+ handler.handle(responses);
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ UUID characteristicUUID = characteristic.getUuid();
+
+ if (characteristicUUID.equals(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID)) {
+ byte[] response = characteristic.getValue();
+ Iterator it = requests.iterator();
+ while (it.hasNext()) {
+ RequestWithHandler rh = it.next();
+ if (rh.request.matches(response)) {
+ it.remove();
+ rh.handler.handle(response);
+ return true;
+ }
+ }
+ LOG.warn("unhandled response: " + Logging.formatBytes(response));
+ }
+
+ return super.onCharacteristicChanged(gatt, characteristic);
+ }
+
public void writeCurrentTime(TransactionBuilder builder, ZonedDateTime time) {
byte[] arr = new byte[11];
arr[0] = FEATURE_CURRENT_TIME;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600DeviceSupport.java
index f980c1432..9f80b2bf9 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600DeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600DeviceSupport.java
@@ -17,19 +17,26 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600;
import java.util.UUID;
-import java.io.IOException;
-
-import android.widget.Toast;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.TextStyle;
+
import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.Casio2C2DSupport;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600.InitOperation;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600.CasioGWB5600TimeZone;
public class CasioGWB5600DeviceSupport extends Casio2C2DSupport {
private static final Logger LOG = LoggerFactory.getLogger(CasioGWB5600DeviceSupport.class);
@@ -61,13 +68,46 @@ public class CasioGWB5600DeviceSupport extends Casio2C2DSupport {
connect();
return builder;
}
- try {
- new InitOperation(this, builder).perform();
- } catch (IOException e) {
- GB.toast(getContext(), "Initializing watch failed", Toast.LENGTH_SHORT, GB.ERROR, e);
- }
+
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
+ requestWorldClocks(builder);
return builder;
}
+ private void requestWorldClocks(TransactionBuilder builder) {
+ HashSet requests = new HashSet();
+
+ for (byte i = 0; i < 6; i++) {
+ requests.addAll(CasioGWB5600TimeZone.requests(i));
+ }
+
+ requestFeatures(builder, requests, responses -> {
+ TransactionBuilder clockBuilder = createTransactionBuilder("setClocks");
+ setClocks(clockBuilder, responses);
+ clockBuilder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
+ clockBuilder.queue(getQueue());
+ });
+ }
+
+ private void setClocks(TransactionBuilder builder, Map responses) {
+ ZoneId tz = ZoneId.systemDefault();
+ Instant now = Instant.now().plusSeconds(2);
+ CasioGWB5600TimeZone[] timezones = {
+ CasioGWB5600TimeZone.fromZoneId(tz, now, tz.getDisplayName(TextStyle.SHORT, Locale.getDefault())),
+ CasioGWB5600TimeZone.fromWatchResponses(responses, 1),
+ CasioGWB5600TimeZone.fromWatchResponses(responses, 2),
+ CasioGWB5600TimeZone.fromWatchResponses(responses, 3),
+ CasioGWB5600TimeZone.fromWatchResponses(responses, 4),
+ CasioGWB5600TimeZone.fromWatchResponses(responses, 5),
+ };
+ for (int i = 5; i >= 0; i--) {
+ if (i%2 == 0)
+ writeAllFeatures(builder, CasioGWB5600TimeZone.dstWatchStateBytes(i, timezones[i], i+1, timezones[i+1]));
+ writeAllFeatures(builder, timezones[i].dstSettingBytes(i));
+ writeAllFeatures(builder, timezones[i].worldCityBytes(i));
+ }
+ writeCurrentTime(builder, ZonedDateTime.ofInstant(now, tz));
+ }
+
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600TimeZone.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600TimeZone.java
index 0a9dedbb8..1801e2fb3 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600TimeZone.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600TimeZone.java
@@ -17,6 +17,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Arrays;
import java.nio.charset.StandardCharsets;
import java.time.DayOfWeek;
import java.time.Instant;
@@ -128,11 +132,12 @@ PONTA DELGADA E4 00 FC 04 02
this.dstSetting = dstSetting;
}
- static public byte[] dstWatchStateRequest(int slot) {
- // request only even slots, the response will also contain the next odd slot
- return new byte[] {
- Casio2C2DSupport.FEATURE_DST_WATCH_STATE,
- (byte) slot};
+ static public Set requests(int slot) {
+ HashSet requests = new HashSet();
+ requests.add(new Casio2C2DSupport.FeatureRequest(Casio2C2DSupport.FEATURE_DST_WATCH_STATE, (byte) (slot/2*2)));
+ requests.add(new Casio2C2DSupport.FeatureRequest(Casio2C2DSupport.FEATURE_DST_SETTING, (byte) slot));
+ requests.add(new Casio2C2DSupport.FeatureRequest(Casio2C2DSupport.FEATURE_WORLD_CITY, (byte) slot));
+ return requests;
}
static public byte[] dstWatchStateBytes(int slotA, CasioGWB5600TimeZone zoneA, int slotB, CasioGWB5600TimeZone zoneB) {
@@ -149,12 +154,6 @@ PONTA DELGADA E4 00 FC 04 02
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff};
}
- static public byte[] dstSettingRequest(int slot) {
- return new byte[] {
- Casio2C2DSupport.FEATURE_DST_SETTING,
- (byte) slot};
- }
-
public byte[] dstSettingBytes(int slot) {
return new byte[] {
Casio2C2DSupport.FEATURE_DST_SETTING,
@@ -166,12 +165,6 @@ PONTA DELGADA E4 00 FC 04 02
dstRules};
}
- static public byte[] worldCityRequest(int slot) {
- return new byte[] {
- Casio2C2DSupport.FEATURE_WORLD_CITY,
- (byte) slot};
- }
-
public byte[] worldCityBytes(int slot) {
byte[] bytes = {
Casio2C2DSupport.FEATURE_WORLD_CITY,
@@ -181,7 +174,7 @@ PONTA DELGADA E4 00 FC 04 02
return bytes;
}
- static CasioGWB5600TimeZone fromWatchResponses(List responses, int slot) {
+ static CasioGWB5600TimeZone fromWatchResponses(Map responses, int slot) {
byte[] name = "unknown".getBytes(StandardCharsets.US_ASCII);
byte[] number = {0,0};
byte offset = 0;
@@ -189,7 +182,7 @@ PONTA DELGADA E4 00 FC 04 02
byte dstRules = 0;
byte dstSetting = 0;
- for (byte[] response: responses) {
+ for (byte[] response: responses.values()) {
if (response[0] == Casio2C2DSupport.FEATURE_DST_WATCH_STATE && response.length >= 9) {
if (response[1] == slot) {
dstSetting = response[3];
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/InitOperation.java
deleted file mode 100644
index b6158fd32..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/InitOperation.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/* Copyright (C) 2023-2024 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.gwb5600;
-
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.time.format.TextStyle;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-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.miband.operations.OperationStatus;
-import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.Casio2C2DSupport;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600.CasioGWB5600DeviceSupport;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600.CasioGWB5600TimeZone;
-
-public class InitOperation extends AbstractBTLEOperation {
- private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class);
-
- private final TransactionBuilder builder;
- private final CasioGWB5600DeviceSupport support;
- private List responses = new LinkedList();
-
- public InitOperation(CasioGWB5600DeviceSupport support, TransactionBuilder builder) {
- super(support);
- this.support = support;
- this.builder = builder;
- builder.setCallback(this);
- }
-
- @Override
- public TransactionBuilder performInitialized(String taskName) throws IOException {
- throw new UnsupportedOperationException("This IS the initialization class, you cannot call this method");
- }
-
- @Override
- protected void doPerform() {//throws IOException {
- builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
- builder.notify(getCharacteristic(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID), true);
- for (int i = 1; i < 6; i++) {
- if (i%2 == 1)
- support.writeAllFeaturesRequest(builder, CasioGWB5600TimeZone.dstWatchStateRequest(i-1));
-
- support.writeAllFeaturesRequest(builder, CasioGWB5600TimeZone.dstSettingRequest(i));
- support.writeAllFeaturesRequest(builder, CasioGWB5600TimeZone.worldCityRequest(i));
- }
- }
-
- @Override
- public boolean onCharacteristicChanged(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic) {
- UUID characteristicUUID = characteristic.getUuid();
- byte[] data = characteristic.getValue();
-
- if (characteristicUUID.equals(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID) && data.length > 0 &&
- (data[0] == Casio2C2DSupport.FEATURE_DST_WATCH_STATE ||
- data[0] == Casio2C2DSupport.FEATURE_DST_SETTING ||
- data[0] == Casio2C2DSupport.FEATURE_WORLD_CITY)) {
- responses.add(data);
- if (responses.size() == 13) {
- TransactionBuilder builder = createTransactionBuilder("setClocks");
- setClocks(builder);
- builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
- builder.setCallback(null);
- builder.queue(support.getQueue());
- operationFinished();
- }
- return true;
- } else {
- LOG.info("Unhandled characteristic changed: " + characteristicUUID);
- return super.onCharacteristicChanged(gatt, characteristic);
- }
- }
-
- private void setClocks(TransactionBuilder builder) {
- ZoneId tz = ZoneId.systemDefault();
- Instant now = Instant.now().plusSeconds(2);
- CasioGWB5600TimeZone[] timezones = {
- CasioGWB5600TimeZone.fromZoneId(tz, now, tz.getDisplayName(TextStyle.SHORT, Locale.getDefault())),
- CasioGWB5600TimeZone.fromWatchResponses(responses, 1),
- CasioGWB5600TimeZone.fromWatchResponses(responses, 2),
- CasioGWB5600TimeZone.fromWatchResponses(responses, 3),
- CasioGWB5600TimeZone.fromWatchResponses(responses, 4),
- CasioGWB5600TimeZone.fromWatchResponses(responses, 5),
- };
- for (int i = 5; i >= 0; i--) {
- if (i%2 == 0)
- support.writeAllFeatures(builder, CasioGWB5600TimeZone.dstWatchStateBytes(i, timezones[i], i+1, timezones[i+1]));
- support.writeAllFeatures(builder, timezones[i].dstSettingBytes(i));
- support.writeAllFeatures(builder, timezones[i].worldCityBytes(i));
- }
- support.writeCurrentTime(builder, ZonedDateTime.ofInstant(now, tz));
- }
-
- @Override
- protected void operationFinished() {
- operationStatus = OperationStatus.FINISHED;
- }
-
-}