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