1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-09 03:37:03 +01:00

Casio GW-B5600: response handlers

This commit is contained in:
Johannes Krude 2023-08-04 23:22:57 +02:00 committed by José Rebelo
parent b6ba421a62
commit 4a9fd49461
6 changed files with 255 additions and 159 deletions

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2024 Andreas Böhler, Andreas Shimokawa, Carsten /* Copyright (C) 2015-2024 Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Damien Gaignon, Daniel Dakhno, Daniele Gobbetti, Frank Ertl, Pfeiffer, Damien Gaignon, Daniel Dakhno, Daniele Gobbetti, Frank Ertl,
José Rebelo José Rebelo, Johannes Krude
This file is part of Gadgetbridge. This file is part of Gadgetbridge.
@ -30,6 +30,7 @@ import androidx.annotation.RequiresApi;
import java.util.Arrays; import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.BondAction; 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.NotifyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.RequestConnectionPriorityAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.RequestConnectionPriorityAction;
@ -118,6 +119,11 @@ public class TransactionBuilder {
return add(action); return add(action);
} }
// Runs the given function/lambda
public TransactionBuilder run(FunctionAction.Function function) {
return add(new FunctionAction(function));
}
public TransactionBuilder add(BtLEAction action) { public TransactionBuilder add(BtLEAction action) {
mTransaction.add(action); mTransaction.add(action);
return this; return this;

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -17,11 +17,22 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio; package nodomain.freeyourgadget.gadgetbridge.service.devices.casio;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.UUID; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants; 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_SETTING_FOR_USER_PROFILE = 0x45;
public static final byte FEATURE_SERVICE_DISCOVERY_MANAGER = 0x47; public static final byte FEATURE_SERVICE_DISCOVERY_MANAGER = 0x47;
private static Logger LOG;
LinkedList<RequestWithHandler> requests = new LinkedList<>();
public Casio2C2DSupport(Logger logger) { public Casio2C2DSupport(Logger logger) {
super(logger); super(logger);
LOG = logger;
}
@Override
public boolean connect() {
requests.clear();
return super.connect();
} }
public void writeAllFeatures(TransactionBuilder builder, byte[] arr) { 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); 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); 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<FeatureRequest, byte[]> 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<FeatureRequest> requests, ResponsesHandler handler) {
HashMap<FeatureRequest, byte[]> responses = new HashMap();
HashSet<FeatureRequest> 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<RequestWithHandler> 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) { public void writeCurrentTime(TransactionBuilder builder, ZonedDateTime time) {
byte[] arr = new byte[11]; byte[] arr = new byte[11];
arr[0] = FEATURE_CURRENT_TIME; arr[0] = FEATURE_CURRENT_TIME;

View File

@ -17,19 +17,26 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600; package nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600;
import java.util.UUID; import java.util.UUID;
import java.io.IOException; import java.util.HashSet;
import java.util.Map;
import android.widget.Toast; import java.util.Locale;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.util.GB;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; 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.devices.casio.CasioConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.Casio2C2DSupport; 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 { public class CasioGWB5600DeviceSupport extends Casio2C2DSupport {
private static final Logger LOG = LoggerFactory.getLogger(CasioGWB5600DeviceSupport.class); private static final Logger LOG = LoggerFactory.getLogger(CasioGWB5600DeviceSupport.class);
@ -61,13 +68,46 @@ public class CasioGWB5600DeviceSupport extends Casio2C2DSupport {
connect(); connect();
return builder; return builder;
} }
try {
new InitOperation(this, builder).perform(); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
} catch (IOException e) { requestWorldClocks(builder);
GB.toast(getContext(), "Initializing watch failed", Toast.LENGTH_SHORT, GB.ERROR, e);
}
return builder; return builder;
} }
private void requestWorldClocks(TransactionBuilder builder) {
HashSet<FeatureRequest> 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<FeatureRequest, byte[]> 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));
}
} }

View File

@ -17,6 +17,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600; 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.nio.charset.StandardCharsets;
import java.time.DayOfWeek; import java.time.DayOfWeek;
import java.time.Instant; import java.time.Instant;
@ -128,11 +132,12 @@ PONTA DELGADA E4 00 FC 04 02
this.dstSetting = dstSetting; this.dstSetting = dstSetting;
} }
static public byte[] dstWatchStateRequest(int slot) { static public Set<Casio2C2DSupport.FeatureRequest> requests(int slot) {
// request only even slots, the response will also contain the next odd slot HashSet<Casio2C2DSupport.FeatureRequest> requests = new HashSet();
return new byte[] { requests.add(new Casio2C2DSupport.FeatureRequest(Casio2C2DSupport.FEATURE_DST_WATCH_STATE, (byte) (slot/2*2)));
Casio2C2DSupport.FEATURE_DST_WATCH_STATE, requests.add(new Casio2C2DSupport.FeatureRequest(Casio2C2DSupport.FEATURE_DST_SETTING, (byte) slot));
(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) { 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}; (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) { public byte[] dstSettingBytes(int slot) {
return new byte[] { return new byte[] {
Casio2C2DSupport.FEATURE_DST_SETTING, Casio2C2DSupport.FEATURE_DST_SETTING,
@ -166,12 +165,6 @@ PONTA DELGADA E4 00 FC 04 02
dstRules}; dstRules};
} }
static public byte[] worldCityRequest(int slot) {
return new byte[] {
Casio2C2DSupport.FEATURE_WORLD_CITY,
(byte) slot};
}
public byte[] worldCityBytes(int slot) { public byte[] worldCityBytes(int slot) {
byte[] bytes = { byte[] bytes = {
Casio2C2DSupport.FEATURE_WORLD_CITY, Casio2C2DSupport.FEATURE_WORLD_CITY,
@ -181,7 +174,7 @@ PONTA DELGADA E4 00 FC 04 02
return bytes; return bytes;
} }
static CasioGWB5600TimeZone fromWatchResponses(List<byte[]> responses, int slot) { static CasioGWB5600TimeZone fromWatchResponses(Map<Casio2C2DSupport.FeatureRequest, byte[]> responses, int slot) {
byte[] name = "unknown".getBytes(StandardCharsets.US_ASCII); byte[] name = "unknown".getBytes(StandardCharsets.US_ASCII);
byte[] number = {0,0}; byte[] number = {0,0};
byte offset = 0; byte offset = 0;
@ -189,7 +182,7 @@ PONTA DELGADA E4 00 FC 04 02
byte dstRules = 0; byte dstRules = 0;
byte dstSetting = 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[0] == Casio2C2DSupport.FEATURE_DST_WATCH_STATE && response.length >= 9) {
if (response[1] == slot) { if (response[1] == slot) {
dstSetting = response[3]; dstSetting = response[3];

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<CasioGWB5600DeviceSupport> {
private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class);
private final TransactionBuilder builder;
private final CasioGWB5600DeviceSupport support;
private List<byte[]> responses = new LinkedList<byte[]>();
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;
}
}