1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-10-06 13:20:30 +02: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
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;

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;
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<RequestWithHandler> 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<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) {
byte[] arr = new byte[11];
arr[0] = FEATURE_CURRENT_TIME;

View File

@ -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<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;
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<Casio2C2DSupport.FeatureRequest> requests(int slot) {
HashSet<Casio2C2DSupport.FeatureRequest> 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<byte[]> responses, int slot) {
static CasioGWB5600TimeZone fromWatchResponses(Map<Casio2C2DSupport.FeatureRequest, byte[]> 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];

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;
}
}