diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f9dcf1c3..dc792ea37 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
* Zepp OS: Add PAI charts
* Bump target SDK version to 31
* Fix media button control for some applications
+* Initial support for Casio GW-B5600
#### 0.75.1
* Fix Weather Notification integration
diff --git a/README.md b/README.md
index 21d67d2d6..6550fc192 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,8 @@ vendor's servers.
- Casio GB-X6900B
- Casio GB-6900B
- Casio GB-5600B
+ - Casio GW-B5600
+ - Casio GMW-B5000 (untested)
- Casio STB-1000
- Casio GBX-100
- Casio GBD-100
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casio/CasioConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casio/CasioConstants.java
index 967c3ebdf..8aa44db8f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casio/CasioConstants.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casio/CasioConstants.java
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018-2021 Andreas Böhler
+/* Copyright (C) 2018-2023 Andreas Böhler, Johannes Krude
This file is part of Gadgetbridge.
@@ -118,6 +118,7 @@ public final class CasioConstants {
MODEL_CASIO_5600B,
MODEL_CASIO_GBX100,
MODEL_CASIO_STB1000,
+ MODEL_CASIO_GWB5600,
}
public enum ConfigurationOption {
@@ -157,6 +158,7 @@ public final class CasioConstants {
put("CASIO_VERSION_INFORMATION", (byte) 0x20);
put("CASIO_DST_WATCH_STATE", (byte) 0x1d);
put("CASIO_DST_SETTING", (byte) 0x1e);
+ put("CASIO_WORLD_CITY", (byte) 0x1f);
put("CASIO_SERVICE_DISCOVERY_MANAGER", (byte) 0x47);
put("CASIO_CURRENT_TIME", (byte) 0x09);
put("CASIO_SETTING_FOR_USER_PROFILE", (byte) 0x45);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casio/gwb5600/CasioGWB5600DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casio/gwb5600/CasioGWB5600DeviceCoordinator.java
new file mode 100644
index 000000000..3d5ccbe23
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casio/gwb5600/CasioGWB5600DeviceCoordinator.java
@@ -0,0 +1,139 @@
+/* Copyright (C) 2023 Johannes Krude
+
+ based on code from BlueWatcher, https://github.com/masterjc/bluewatcher
+
+ 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.devices.casio.gwb5600;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import android.app.Activity;
+import android.content.Context;
+import android.net.Uri;
+import android.bluetooth.le.ScanFilter;
+import android.os.ParcelUuid;
+
+import androidx.annotation.NonNull;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
+import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioDeviceCoordinator;
+
+public class CasioGWB5600DeviceCoordinator extends CasioDeviceCoordinator {
+
+ @NonNull
+ @Override
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
+ String name = candidate.getDevice().getName();
+ if (name != null) {
+ if (name.equals("CASIO GW-B5600") ||
+ name.equals("CASIO GMW-B5000")) {
+ return DeviceType.CASIOGWB5600;
+ }
+ }
+
+ return DeviceType.UNKNOWN;
+ }
+
+ @Override
+ public DeviceType getDeviceType() {
+ return DeviceType.CASIOGWB5600;
+ }
+
+ @Override
+ public int getBondingStyle(){
+ return BONDING_STYLE_BOND;
+ }
+
+ @Override
+ public Class extends Activity> getPairingActivity() {
+ return null;
+ }
+
+ @Override
+ protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
+ }
+
+ @Override
+ public int getAlarmSlotCount(GBDevice device) {
+ return 0;
+ }
+
+ @Override
+ public boolean supportsCalendarEvents() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsRealtimeData() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsFindDevice() {
+ return false;
+ }
+
+ @Override
+ public InstallHandler findInstallHandler(Uri uri, Context context) {
+ return null;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsActivityTracking() {
+ return false;
+ }
+
+ @Override
+ public SampleProvider extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
+ return null;
+ }
+
+ @Override
+ public boolean supportsScreenshots() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsSmartWakeup(GBDevice device) {
+ return false;
+ }
+
+ @Override
+ public boolean supportsAppsManagement(final GBDevice device) {
+ return false;
+ }
+
+ @Override
+ public Class extends Activity> getAppsManagementActivity() {
+ return null;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
index 54a625de5..24968cc2b 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
@@ -1,8 +1,9 @@
-/* Copyright (C) 2015-2020 Andreas Böhler, Andreas Shimokawa, Carsten
+/* Copyright (C) 2015-2023 Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Cre3per, Daniel Dakhno, Daniele Gobbetti, Gordon Williams,
Jean-François Greffier, João Paulo Barraca, José Rebelo, Kranz, ladbsoft,
Manuel Ruß, maxirnilian, Pavel, Pavel Elagin, protomors, Quallenauge,
- Sami Alaoui, Sebastian Kranz, Sophanimus, tiparega, Vadim Kaushan
+ Sami Alaoui, Sebastian Kranz, Sophanimus, tiparega, Vadim Kaushan,
+ Johannes Krude
This file is part of Gadgetbridge.
@@ -92,6 +93,7 @@ public enum DeviceType {
ROIDMI3(112, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi3),
CASIOGB6900(120, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_casiogb6900),
CASIOGBX100(121, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_casiogbx100),
+ CASIOGWB5600(122, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_casiogbx100),
MISCALE2(131, R.drawable.ic_device_miscale2, R.drawable.ic_device_miscale2_disabled, R.string.devicetype_miscale2),
BFH16(140, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_bfh16),
MAKIBESHR3(150, R.drawable.ic_device_default, R.drawable.ic_device_hplus_disabled, R.string.devicetype_makibes_hr3),
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
index 99bd02638..70121fa4e 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
@@ -34,6 +34,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDev
import nodomain.freeyourgadget.gadgetbridge.service.devices.binary_sensor.BinarySensorSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gb6900.CasioGB6900DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gbx100.CasioGBX100DeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600.CasioGWB5600DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.domyos.DomyosT540Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.fitpro.FitProDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.flipper.zero.support.FlipperZeroSupport;
@@ -293,6 +294,8 @@ public class DeviceSupportFactory {
return new ServiceDeviceSupport(new CasioGB6900DeviceSupport());
case CASIOGBX100:
return new ServiceDeviceSupport(new CasioGBX100DeviceSupport());
+ case CASIOGWB5600:
+ return new ServiceDeviceSupport(new CasioGWB5600DeviceSupport());
case MISCALE2:
return new ServiceDeviceSupport(new MiScale2DeviceSupport());
case BFH16:
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
index d3d385a00..7de4aed26 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
@@ -1,5 +1,6 @@
-/* Copyright (C) 2015-2021 Andreas Böhler, Andreas Shimokawa, Carsten
- Pfeiffer, Daniel Dakhno, Daniele Gobbetti, JohnnySun, José Rebelo
+/* Copyright (C) 2015-2023 Andreas Böhler, Andreas Shimokawa, Carsten
+ Pfeiffer, Daniel Dakhno, Daniele Gobbetti, JohnnySun, José Rebelo,
+ Johannes Krude
This file is part of Gadgetbridge.
@@ -82,6 +83,12 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
return mQueue.connect();
}
+ public void disconnect() {
+ if (mQueue != null) {
+ mQueue.disconnect();
+ }
+ }
+
@Override
public void setAutoReconnect(boolean enable) {
super.setAutoReconnect(enable);
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
new file mode 100644
index 000000000..17f9dcc33
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600DeviceSupport.java
@@ -0,0 +1,64 @@
+/* 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.devices.casio.gwb5600;
+
+import java.util.UUID;
+import java.io.IOException;
+
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.Casio2C2DSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600.InitOperation;
+
+public class CasioGWB5600DeviceSupport extends Casio2C2DSupport {
+ private static final Logger LOG = LoggerFactory.getLogger(CasioGWB5600DeviceSupport.class);
+
+ public CasioGWB5600DeviceSupport() {
+ super(LOG);
+ }
+
+ @Override
+ public boolean useAutoConnect() {
+ return false;
+ }
+
+ @Override
+ protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
+ // remove this workaround once Gadgetbridge does discovery on initial pairing
+ if (getCharacteristic(CasioConstants.CASIO_READ_REQUEST_FOR_ALL_FEATURES_CHARACTERISTIC_UUID) == null ||
+ getCharacteristic(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID) == null) {
+ LOG.info("Reconnecting to discover characteristics");
+ disconnect();
+ connect();
+ return builder;
+ }
+ try {
+ new InitOperation(this, builder).perform();
+ } catch (IOException e) {
+ GB.toast(getContext(), "Initializing watch failed", Toast.LENGTH_SHORT, GB.ERROR, e);
+ }
+
+ return builder;
+ }
+}
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
new file mode 100644
index 000000000..d00aa827d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/CasioGWB5600TimeZone.java
@@ -0,0 +1,421 @@
+/* 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.devices.casio.gwb5600;
+
+import java.util.List;
+import java.util.Arrays;
+import java.nio.charset.StandardCharsets;
+
+import org.threeten.bp.Instant;
+import org.threeten.bp.LocalTime;
+import org.threeten.bp.Month;
+import org.threeten.bp.DayOfWeek;
+import org.threeten.bp.ZoneId;
+import org.threeten.bp.ZoneOffset;
+import org.threeten.bp.zone.ZoneRules;
+import org.threeten.bp.zone.ZoneOffsetTransition;
+import org.threeten.bp.zone.ZoneOffsetTransitionRule;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
+
+public class CasioGWB5600TimeZone {
+
+/*
+There are six clocks on the Casio GW-B5600
+0 is the main clock
+1-5 are the world clocks
+
+0x1d 00 01 DST0 DST1 TZ0A TZ0B TZ1A TZ1B ff ff ff ff ff
+0x1d 02 03 DST2 DST3 TZ2A TZ2B TZ3A TZ3B ff ff ff ff ff
+0x1d 04 05 DST4 DST5 TZ4A TZ4B TZ5A TZ5B ff ff ff ff ff
+DST: bitwise flags; bit0: DST on, bit1: DST auto
+
+0x1e 0-5 TZ_A TZ_B TZ_OFF TZ_DSTOFF TZ_DSTRULES
+A/B seem to be ignored by the watch
+OFF & DSTOFF in 15 minute intervals
+
+0x1f 0-5 (18 bytes ASCII TZ name)
+
+Timezones selectable on the watch:
+ A B OFF DSTOFF DSTRULES
+BAKER ISLAND 39 01 D0 04 00
+PAGO PAGO D7 00 D4 04 00
+HONOLULU 7B 00 D8 04 00
+MARQUESAS ISLANDS 3A 01 DA 04 00
+ANCHORAGE 0C 00 DC 04 01
+LOS ANGELES A1 00 E0 04 01
+DENVER 54 00 E4 04 01
+CHICAGO 42 00 E8 04 01
+NEW YORK CA 00 EC 04 01
+HALIFAX 71 00 F0 04 01
+ST.JOHN'S 0C 01 F2 04 01
+RIO DE JANEIRO F1 00 F4 04 00
+F.DE NORONHA 62 00 F8 04 00
+PRAIA E9 00 FC 04 00
+UTC 00 00 00 00 00
+LONDON A0 00 00 04 02
+PARIS DC 00 04 04 02
+ATHENS 13 00 08 04 02
+JEDDAH 85 00 0C 04 00
+TEHRAN 16 01 0E 04 2B
+DUBAI 5B 00 10 04 00
+KABUL 88 00 12 04 00
+KARACHI 8B 00 14 04 00
+DELHI 52 00 16 04 00
+KATHMANDU 8C 00 17 04 00
+DHAKA 56 00 18 04 00
+YANGON 2F 01 1A 04 00
+BANGKOK 1C 00 1C 04 00
+HONG KONG 7A 00 20 04 00
+PYONGYANG EA 00 24 04 00
+EUCLA 36 01 23 04 00
+TOKYO 19 01 24 04 00
+ADELAIDE 05 00 26 04 04
+SYDNEY 0F 01 28 04 04
+LORD HOWE ISLAND 37 01 2A 02 12
+NOUMEA CD 00 2C 04 00
+WELLINGTON 2B 01 30 04 05
+CHATHAM ISLANDS 3F 00 33 04 17
+NUKUALOFA D0 00 34 04 00
+KIRITIMATI 93 00 38 04 00
+
+Timezones NOT selectable on the watch:
+ A B OFF DSTOFF DSTRULES
+CASABLANCA 3A 00 00 04 0F
+BEIRUT 22 00 08 04 0C
+JERUSALEM 86 00 08 04 2A
+NORFOLK ISLAND 38 01 2C 04 04
+EASTER ISLAND 5E 00 E8 04 1C
+HAVANA 75 00 EC 04 15
+SANTIAGO 02 01 F0 04 1B
+ASUNCION 12 00 F0 04 09
+PONTA DELGADA E4 00 FC 04 02
+
+*/
+
+ private byte[] name;
+ private byte[] number;
+ private byte offset;
+ private byte dstOffset;
+ private byte dstRules;
+ private byte dstSetting;
+
+ // bitwise flags
+ final static byte DST_SETTING_ON = 1;
+ final static byte DST_SETTING_AUTO = 2;
+
+ private CasioGWB5600TimeZone(byte[] name, byte[] number, byte offset, byte dstOffset, byte dstRules, byte dstSetting) {
+ this.name = name;
+ this.number = number;
+ this.offset = offset;
+ this.dstOffset = dstOffset;
+ this.dstRules = dstRules;
+ 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[] {
+ CasioConstants.characteristicToByte.get("CASIO_DST_WATCH_STATE"),
+ (byte) slot};
+ }
+
+ static public byte[] dstWatchStateBytes(int slotA, CasioGWB5600TimeZone zoneA, int slotB, CasioGWB5600TimeZone zoneB) {
+ return new byte[] {
+ CasioConstants.characteristicToByte.get("CASIO_DST_WATCH_STATE"),
+ (byte) slotA,
+ (byte) slotB,
+ zoneA.dstSetting,
+ zoneB.dstSetting,
+ zoneA.number[0],
+ zoneA.number[1],
+ zoneB.number[0],
+ zoneB.number[1],
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff};
+ }
+
+ static public byte[] dstSettingRequest(int slot) {
+ return new byte[] {
+ CasioConstants.characteristicToByte.get("CASIO_DST_SETTING"),
+ (byte) slot};
+ }
+
+ public byte[] dstSettingBytes(int slot) {
+ return new byte[] {
+ CasioConstants.characteristicToByte.get("CASIO_DST_SETTING"),
+ (byte) slot,
+ number[0],
+ number[1],
+ offset,
+ dstOffset,
+ dstRules};
+ }
+
+ static public byte[] worldCityRequest(int slot) {
+ return new byte[] {
+ CasioConstants.characteristicToByte.get("CASIO_WORLD_CITY"),
+ (byte) slot};
+ }
+
+ public byte[] worldCityBytes(int slot) {
+ byte[] bytes = {
+ CasioConstants.characteristicToByte.get("CASIO_WORLD_CITY"),
+ (byte) slot,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
+ System.arraycopy(name, 0, bytes, 2, Math.min(name.length, 18));
+ return bytes;
+ }
+
+ static CasioGWB5600TimeZone fromWatchResponses(List responses, int slot) {
+ byte[] name = "unknown".getBytes(StandardCharsets.US_ASCII);
+ byte[] number = {0,0};
+ byte offset = 0;
+ byte dstOffset = 0;
+ byte dstRules = 0;
+ byte dstSetting = 0;
+
+ for (byte[] response: responses) {
+ if (response[0] == CasioConstants.characteristicToByte.get("CASIO_DST_WATCH_STATE") && response.length >= 9) {
+ if (response[1] == slot) {
+ dstSetting = response[3];
+ number = new byte[] {response[5], response[6]};
+ }
+ if (response[2] == slot) {
+ dstSetting = response[4];
+ number = new byte[] {response[7], response[8]};
+ }
+ } else if (response[0] == CasioConstants.characteristicToByte.get("CASIO_DST_SETTING") && response.length >= 7 && response[1] == slot) {
+ number = new byte[] {response[2], response[3]};
+ offset = response[4];
+ dstOffset = response[5];
+ dstRules = response[6];
+ } else if (response[0] == CasioConstants.characteristicToByte.get("CASIO_WORLD_CITY") && response.length >= 2 && response[1] == slot) {
+ int size;
+ for (size = 0; size < response.length-2; size++) {
+ if (response[2+size] == 0) {
+ break;
+ }
+ }
+ name = Arrays.copyOfRange(response, 2, 2+size);
+ }
+ }
+
+ return new CasioGWB5600TimeZone(name, number, offset, dstOffset, dstRules, dstSetting);
+ }
+
+ static CasioGWB5600TimeZone fromZoneId(ZoneId zone, Instant time, String zoneName) {
+ ZoneRules rules = zone.getRules();
+
+ byte[] name = zoneName.getBytes(StandardCharsets.US_ASCII);
+ byte[] number = {0,0};
+ byte offset = (byte) (rules.getStandardOffset(time).getTotalSeconds() / 60 / 15);
+ byte dstOffset = 0; // can be set only later once we now the next transition
+ byte dstRules = 0;
+ byte dstSetting = 0;
+
+ ZoneOffsetTransition next = rules.nextTransition(time);
+ int nextYear = next.getInstant().atZone(zone).getYear();
+ ZoneOffsetTransition next2 = (next == null ? null: rules.nextTransition(next.getInstant()));
+ int next2Year = (next2 == null ? 0 : next2.getInstant().atZone(zone).getYear());
+
+ if (next == null) {
+ // no DST is easy
+ dstSetting = DST_SETTING_AUTO;
+ } else {
+ // we need an Instant with DST on to get the dstOffset
+ if (rules.isDaylightSavings(time)) {
+ dstOffset = (byte) (rules.getDaylightSavings(time).getSeconds() / 60 / 15);
+ } else {
+ dstOffset = (byte) (rules.getDaylightSavings(next.getInstant().plusSeconds(1)).getSeconds() / 60 / 15);
+ }
+ // find Watch DST rules
+ dstRules = findWatchDstRules(offset, dstOffset, next, nextYear, next2, next2Year);
+ if (dstRules != 0) {
+ // DST AUTO if the watch knows at least the next transition
+ // otherwise will result in incorrect time between actual DST change and next sync
+ dstSetting |= DST_SETTING_AUTO;
+ }
+ // if DST bit is incorrect, the watch will substract or add time
+ if (rules.isDaylightSavings(time)) {
+ dstSetting |= DST_SETTING_ON;
+ }
+ }
+
+ return new CasioGWB5600TimeZone(name, number, offset, dstOffset, dstRules, dstSetting);
+ }
+
+ // We are searching for watch DST rules which match the next two transitions.
+ // In case only the next transition matches a rule, the rule is still used
+ static byte findWatchDstRules(byte offset, byte dstOffset, ZoneOffsetTransition next, int nextYear, ZoneOffsetTransition next2, int next2Year) {
+ WatchDstRules candidate = null;
+ for (WatchDstRules r: watchDstRules) {
+ int match = r.matches(offset, dstOffset, next, nextYear, next2, next2Year);
+ if (match == 2)
+ return r.dstRules;
+ if (match == 1 && candidate == null)
+ candidate = r;
+ }
+ if (candidate != null)
+ return candidate.dstRules;
+ return (byte) 0;
+ }
+
+ static class WatchDstRules {
+ final byte offset;
+ final byte dstOffset;
+ final byte dstRules;
+ final ZoneOffsetTransitionRule ruleA;
+ final ZoneOffsetTransitionRule ruleB;
+
+ WatchDstRules(int offset, int dstOffset, int dstRules, ZoneOffsetTransitionRule ruleA, ZoneOffsetTransitionRule ruleB) {
+ this.offset = (byte) offset;
+ this.dstOffset = (byte) dstOffset;
+ this.dstRules = (byte) dstRules;
+ this.ruleA = ruleA;
+ this.ruleB = ruleB;
+ }
+
+ // returns how many of the next transitions match the rules
+ int matches(byte offset, byte dstOffset, ZoneOffsetTransition next, int nextYear, ZoneOffsetTransition next2, int next2Year) {
+ if (offset != this.offset || dstOffset != this.dstOffset)
+ return -1;
+ if (this.ruleA.createTransition(nextYear).equals(next)) {
+ if (this.ruleB.createTransition(next2Year).equals(next2))
+ return 2;
+ return 1;
+ }
+ if (this.ruleB.createTransition(nextYear).equals(next)) {
+ if (this.ruleA.createTransition(next2Year).equals(next2))
+ return 2;
+ return 1;
+ }
+ return 0;
+ }
+ }
+
+ // All known Watch DST Rules
+ // Possibly incomplete and incorrect
+ //
+ // When adding new WatchDstRules, test them:
+ // 1. Apply the following changes to CasioGWB5600InitOperation
+ // + static int nextNext = 0;
+ // private void setClocks(TransactionBuilder builder) {
+ // ZoneId tz = ZoneId.systemDefault();
+ // Instant now = Instant.now().plusSeconds(2);
+ // + now = tz.getRules().nextTransition(now).getInstant();
+ // + if (nextNext != 0)
+ // + now = tz.getRules().nextTransition(now).getInstant();
+ // + nextNext ^= 1;
+ // + now = now.minusSeconds(10);
+ // 2. Sync the time on the watch and observe a DST change
+ // 3. Repeat the time sync to obseve a second DST change
+ static final WatchDstRules[] watchDstRules = {
+ // Europe/London
+ new WatchDstRules(0x00, 0x04, 0x02,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(0), ZoneOffset.ofTotalSeconds(0), ZoneOffset.ofTotalSeconds(3600)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(0), ZoneOffset.ofTotalSeconds(3600), ZoneOffset.ofTotalSeconds(0))),
+ // Europe/Paris
+ new WatchDstRules(0x04, 0x04, 0x02,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(3600), ZoneOffset.ofTotalSeconds(3600), ZoneOffset.ofTotalSeconds(7200)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(3600), ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(3600))),
+ // Europe/Athen
+ new WatchDstRules(0x08, 0x04, 0x02,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800), ZoneOffset.ofTotalSeconds(7200))),
+ // Asia/Beirut
+ new WatchDstRules(0x08, 0x04, 0x0C,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800), ZoneOffset.ofTotalSeconds(7200))),
+ // Asia/Jerusalem
+ new WatchDstRules(0x08, 0x04, 0x2A,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 23, DayOfWeek.FRIDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(7200), ZoneOffset.ofTotalSeconds(10800), ZoneOffset.ofTotalSeconds(7200))),
+ // Australia/Adelaide
+ new WatchDstRules(0x26, 0x04, 0x04,
+ ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(34200), ZoneOffset.ofTotalSeconds(37800), ZoneOffset.ofTotalSeconds(34200)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(34200), ZoneOffset.ofTotalSeconds(34200), ZoneOffset.ofTotalSeconds(37800))),
+ // Australia/Sydney
+ new WatchDstRules(0x28, 0x04, 0x04,
+ ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(36000), ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(36000)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(36000), ZoneOffset.ofTotalSeconds(36000), ZoneOffset.ofTotalSeconds(39600))),
+ // Australia/Lord_Howe
+ new WatchDstRules(0x2A, 0x02, 0x12,
+ ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(37800), ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(37800)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(37800), ZoneOffset.ofTotalSeconds(37800), ZoneOffset.ofTotalSeconds(39600))),
+ // Pacific/Norfolk
+ new WatchDstRules(0x2C, 0x04, 0x04,
+ ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(43200), ZoneOffset.ofTotalSeconds(39600)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(39600), ZoneOffset.ofTotalSeconds(43200))),
+ // Pacific/Auckland
+ new WatchDstRules(0x30, 0x04, 0x05,
+ ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(43200), ZoneOffset.ofTotalSeconds(46800), ZoneOffset.ofTotalSeconds(43200)),
+ ZoneOffsetTransitionRule.of(Month.SEPTEMBER, 24, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(43200), ZoneOffset.ofTotalSeconds(43200), ZoneOffset.ofTotalSeconds(46800))),
+ // Pacific/Chatham
+ new WatchDstRules(0x33, 0x04, 0x17,
+ ZoneOffsetTransitionRule.of(Month.APRIL, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 45), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(45900), ZoneOffset.ofTotalSeconds(49500), ZoneOffset.ofTotalSeconds(45900)),
+ ZoneOffsetTransitionRule.of(Month.SEPTEMBER, 24, DayOfWeek.SUNDAY, LocalTime.of(2, 45), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(45900), ZoneOffset.ofTotalSeconds(45900), ZoneOffset.ofTotalSeconds(49500))),
+ // America/Anchorage
+ new WatchDstRules(0xDC, 0x04, 0x01,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-32400), ZoneOffset.ofTotalSeconds(-32400), ZoneOffset.ofTotalSeconds(-28800)),
+ ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-32400), ZoneOffset.ofTotalSeconds(-28800), ZoneOffset.ofTotalSeconds(-32400))),
+ // America/Los_Angeles
+ new WatchDstRules(0xE0, 0x04, 0x01,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-28800), ZoneOffset.ofTotalSeconds(-28800), ZoneOffset.ofTotalSeconds(-25200)),
+ ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-28800), ZoneOffset.ofTotalSeconds(-25200), ZoneOffset.ofTotalSeconds(-28800))),
+ // America/Denver
+ new WatchDstRules(0xE4, 0x04, 0x01,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-25200), ZoneOffset.ofTotalSeconds(-25200), ZoneOffset.ofTotalSeconds(-21600)),
+ ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-25200), ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-25200))),
+ // America/Chicago
+ new WatchDstRules(0xE8, 0x04, 0x01,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-18000)),
+ ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-21600))),
+ // Chile/EasterIsland
+ new WatchDstRules(0xE8, 0x04, 0x1C,
+ ZoneOffsetTransitionRule.of(Month.APRIL, 2, DayOfWeek.SUNDAY, LocalTime.of(3, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-21600)),
+ ZoneOffsetTransitionRule.of(Month.SEPTEMBER, 2, DayOfWeek.SUNDAY, LocalTime.of(4, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-21600), ZoneOffset.ofTotalSeconds(-18000))),
+ // America/New_York
+ new WatchDstRules(0xEC, 0x04, 0x01,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-14400)),
+ ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-18000))),
+ // America/Havana
+ new WatchDstRules(0xEC, 0x04, 0x15,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-14400)),
+ ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.STANDARD, ZoneOffset.ofTotalSeconds(-18000), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-18000))),
+ // America/Santiago
+ new WatchDstRules(0xF0, 0x04, 0x1B,
+ ZoneOffsetTransitionRule.of(Month.APRIL, 2, DayOfWeek.SUNDAY, LocalTime.of(3, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800), ZoneOffset.ofTotalSeconds(-14400)),
+ ZoneOffsetTransitionRule.of(Month.SEPTEMBER, 2, DayOfWeek.SUNDAY, LocalTime.of(4, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800))),
+ // America/Halifax
+ new WatchDstRules(0xF0, 0x04, 0x01,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800)),
+ ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800), ZoneOffset.ofTotalSeconds(-14400))),
+ // America/Asuncion
+ new WatchDstRules(0xF0, 0x04, 0x09,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 22, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800), ZoneOffset.ofTotalSeconds(-14400)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 1, DayOfWeek.SUNDAY, LocalTime.of(0, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-14400), ZoneOffset.ofTotalSeconds(-10800))),
+ // America/St_Johns
+ new WatchDstRules(0xF2, 0x04, 0x01,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 8, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-12600), ZoneOffset.ofTotalSeconds(-12600), ZoneOffset.ofTotalSeconds(-9000)),
+ ZoneOffsetTransitionRule.of(Month.NOVEMBER, 1, DayOfWeek.SUNDAY, LocalTime.of(2, 0), false, ZoneOffsetTransitionRule.TimeDefinition.WALL, ZoneOffset.ofTotalSeconds(-12600), ZoneOffset.ofTotalSeconds(-9000), ZoneOffset.ofTotalSeconds(-12600))),
+ // Atlantic/Azores
+ new WatchDstRules(0xFC, 0x04, 0x02,
+ ZoneOffsetTransitionRule.of(Month.MARCH, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-3600), ZoneOffset.ofTotalSeconds(-3600), ZoneOffset.ofTotalSeconds(0)),
+ ZoneOffsetTransitionRule.of(Month.OCTOBER, 25, DayOfWeek.SUNDAY, LocalTime.of(1, 0), false, ZoneOffsetTransitionRule.TimeDefinition.UTC, ZoneOffset.ofTotalSeconds(-3600), ZoneOffset.ofTotalSeconds(0), ZoneOffset.ofTotalSeconds(-3600))),
+ };
+}
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
new file mode 100644
index 000000000..0b6ca535f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casio/gwb5600/InitOperation.java
@@ -0,0 +1,129 @@
+/* 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.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.util.List;
+import java.util.LinkedList;
+import java.util.UUID;
+import java.util.Locale;
+
+import org.threeten.bp.Instant;
+import org.threeten.bp.ZoneId;
+import org.threeten.bp.ZonedDateTime;
+import org.threeten.bp.format.TextStyle;
+
+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.devices.miband.operations.OperationStatus;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
+import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
+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] == CasioConstants.characteristicToByte.get("CASIO_DST_WATCH_STATE") ||
+ data[0] == CasioConstants.characteristicToByte.get("CASIO_DST_SETTING") ||
+ data[0] == CasioConstants.characteristicToByte.get("CASIO_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;
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
index 88f7b0939..aada6b2c4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
@@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gbx100.CasioGBX100DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGWB5600DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBuds2DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.flipper.zero.FlipperZeroCoordinator;
@@ -336,6 +337,7 @@ public class DeviceHelper {
result.add(new Y5Coordinator());
result.add(new CasioGB6900DeviceCoordinator());
result.add(new CasioGBX100DeviceCoordinator());
+ result.add(new CasioGWB5600DeviceCoordinator());
result.add(new BFH16DeviceCoordinator());
result.add(new MijiaLywsd02Coordinator());
result.add(new ITagCoordinator());
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 289a3f7eb..7213eaa48 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1291,6 +1291,7 @@
Y5
Casio GB-6900
Casio GBX-100
+ Casio GW-B5600
Mi Scale 2
iTag
BFH-16
@@ -1523,7 +1524,7 @@
Contributors
Andreas Shimokawa\nCarsten Pfeiffer\nDaniele Gobbetti
Additional device support
- João Paulo Barraca (HPlus)\nVitaly Svyastyn (NO.1 F1)\nSami Alaoui (Teclast H30)\n“ladbsoft” (XWatch)\nSebastian Kranz (ZeTime)\nVadim Kaushan (ID115)\n“maxirnilian” (Lenovo Watch 9)\n“ksiwczynski”, “mkusnierz”, “mamutcho” (Lenovo Watch X Plus)\nAndreas Böhler (Casio GB-6900B, Casio GB-5600B, Casio GBX-100)\nJean-François Greffier (Mi Scale 2)\nJohannes Schmitt (BFH-16)\nLukas Schwichtenberg (Makibes HR3)\nDaniel Dakhno (Fossil Q Hybrid, Fossil Hybrid HR)\nGordon Williams (Bangle.js)\nPavel Elagin (JYou Y5)\nTaavi Eomäe (iTag)
+ João Paulo Barraca (HPlus)\nVitaly Svyastyn (NO.1 F1)\nSami Alaoui (Teclast H30)\n“ladbsoft” (XWatch)\nSebastian Kranz (ZeTime)\nVadim Kaushan (ID115)\n“maxirnilian” (Lenovo Watch 9)\n“ksiwczynski”, “mkusnierz”, “mamutcho” (Lenovo Watch X Plus)\nAndreas Böhler (Casio GB-6900B, Casio GB-5600B, Casio GBX-100)\nJean-François Greffier (Mi Scale 2)\nJohannes Schmitt (BFH-16)\nLukas Schwichtenberg (Makibes HR3)\nDaniel Dakhno (Fossil Q Hybrid, Fossil Hybrid HR)\nGordon Williams (Bangle.js)\nPavel Elagin (JYou Y5)\nTaavi Eomäe (iTag)\nJohannes Krude(Casio GW-B5600)
Many thanks to all unlisted contributors for contributing code, translations, support, ideas, motivation, bug reports, money… ✊
Links
All these permissions are required and instability might occur if not granted