mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-26 02:25:50 +01:00
Casio GW-B5600: initial support
This commit is contained in:
parent
91415e2500
commit
7cb060c9fe
@ -8,6 +8,7 @@
|
|||||||
* Zepp OS: Add PAI charts
|
* Zepp OS: Add PAI charts
|
||||||
* Bump target SDK version to 31
|
* Bump target SDK version to 31
|
||||||
* Fix media button control for some applications
|
* Fix media button control for some applications
|
||||||
|
* Initial support for Casio GW-B5600
|
||||||
|
|
||||||
#### 0.75.1
|
#### 0.75.1
|
||||||
* Fix Weather Notification integration
|
* Fix Weather Notification integration
|
||||||
|
@ -51,6 +51,8 @@ vendor's servers.
|
|||||||
- Casio GB-X6900B
|
- Casio GB-X6900B
|
||||||
- Casio GB-6900B
|
- Casio GB-6900B
|
||||||
- Casio GB-5600B
|
- Casio GB-5600B
|
||||||
|
- Casio GW-B5600
|
||||||
|
- Casio GMW-B5000 (untested)
|
||||||
- Casio STB-1000
|
- Casio STB-1000
|
||||||
- Casio GBX-100
|
- Casio GBX-100
|
||||||
- Casio GBD-100
|
- Casio GBD-100
|
||||||
|
@ -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.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -118,6 +118,7 @@ public final class CasioConstants {
|
|||||||
MODEL_CASIO_5600B,
|
MODEL_CASIO_5600B,
|
||||||
MODEL_CASIO_GBX100,
|
MODEL_CASIO_GBX100,
|
||||||
MODEL_CASIO_STB1000,
|
MODEL_CASIO_STB1000,
|
||||||
|
MODEL_CASIO_GWB5600,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ConfigurationOption {
|
public enum ConfigurationOption {
|
||||||
@ -157,6 +158,7 @@ public final class CasioConstants {
|
|||||||
put("CASIO_VERSION_INFORMATION", (byte) 0x20);
|
put("CASIO_VERSION_INFORMATION", (byte) 0x20);
|
||||||
put("CASIO_DST_WATCH_STATE", (byte) 0x1d);
|
put("CASIO_DST_WATCH_STATE", (byte) 0x1d);
|
||||||
put("CASIO_DST_SETTING", (byte) 0x1e);
|
put("CASIO_DST_SETTING", (byte) 0x1e);
|
||||||
|
put("CASIO_WORLD_CITY", (byte) 0x1f);
|
||||||
put("CASIO_SERVICE_DISCOVERY_MANAGER", (byte) 0x47);
|
put("CASIO_SERVICE_DISCOVERY_MANAGER", (byte) 0x47);
|
||||||
put("CASIO_CURRENT_TIME", (byte) 0x09);
|
put("CASIO_CURRENT_TIME", (byte) 0x09);
|
||||||
put("CASIO_SETTING_FOR_USER_PROFILE", (byte) 0x45);
|
put("CASIO_SETTING_FOR_USER_PROFILE", (byte) 0x45);
|
||||||
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
Pfeiffer, Cre3per, Daniel Dakhno, Daniele Gobbetti, Gordon Williams,
|
||||||
Jean-François Greffier, João Paulo Barraca, José Rebelo, Kranz, ladbsoft,
|
Jean-François Greffier, João Paulo Barraca, José Rebelo, Kranz, ladbsoft,
|
||||||
Manuel Ruß, maxirnilian, Pavel, Pavel Elagin, protomors, Quallenauge,
|
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.
|
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),
|
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),
|
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),
|
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),
|
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),
|
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),
|
MAKIBESHR3(150, R.drawable.ic_device_default, R.drawable.ic_device_hplus_disabled, R.string.devicetype_makibes_hr3),
|
||||||
|
@ -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.binary_sensor.BinarySensorSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gb6900.CasioGB6900DeviceSupport;
|
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.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.domyos.DomyosT540Support;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.fitpro.FitProDeviceSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.fitpro.FitProDeviceSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.flipper.zero.support.FlipperZeroSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.flipper.zero.support.FlipperZeroSupport;
|
||||||
@ -293,6 +294,8 @@ public class DeviceSupportFactory {
|
|||||||
return new ServiceDeviceSupport(new CasioGB6900DeviceSupport());
|
return new ServiceDeviceSupport(new CasioGB6900DeviceSupport());
|
||||||
case CASIOGBX100:
|
case CASIOGBX100:
|
||||||
return new ServiceDeviceSupport(new CasioGBX100DeviceSupport());
|
return new ServiceDeviceSupport(new CasioGBX100DeviceSupport());
|
||||||
|
case CASIOGWB5600:
|
||||||
|
return new ServiceDeviceSupport(new CasioGWB5600DeviceSupport());
|
||||||
case MISCALE2:
|
case MISCALE2:
|
||||||
return new ServiceDeviceSupport(new MiScale2DeviceSupport());
|
return new ServiceDeviceSupport(new MiScale2DeviceSupport());
|
||||||
case BFH16:
|
case BFH16:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/* Copyright (C) 2015-2021 Andreas Böhler, Andreas Shimokawa, Carsten
|
/* Copyright (C) 2015-2023 Andreas Böhler, Andreas Shimokawa, Carsten
|
||||||
Pfeiffer, Daniel Dakhno, Daniele Gobbetti, JohnnySun, José Rebelo
|
Pfeiffer, Daniel Dakhno, Daniele Gobbetti, JohnnySun, José Rebelo,
|
||||||
|
Johannes Krude
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -82,6 +83,12 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
|
|||||||
return mQueue.connect();
|
return mQueue.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void disconnect() {
|
||||||
|
if (mQueue != null) {
|
||||||
|
mQueue.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAutoReconnect(boolean enable) {
|
public void setAutoReconnect(boolean enable) {
|
||||||
super.setAutoReconnect(enable);
|
super.setAutoReconnect(enable);
|
||||||
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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<byte[]> 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))),
|
||||||
|
};
|
||||||
|
}
|
@ -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 <http://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.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<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] == 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gbx100.CasioGBX100DeviceCoordinator;
|
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.domyos.DomyosT540Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBuds2DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBuds2DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.flipper.zero.FlipperZeroCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.flipper.zero.FlipperZeroCoordinator;
|
||||||
@ -336,6 +337,7 @@ public class DeviceHelper {
|
|||||||
result.add(new Y5Coordinator());
|
result.add(new Y5Coordinator());
|
||||||
result.add(new CasioGB6900DeviceCoordinator());
|
result.add(new CasioGB6900DeviceCoordinator());
|
||||||
result.add(new CasioGBX100DeviceCoordinator());
|
result.add(new CasioGBX100DeviceCoordinator());
|
||||||
|
result.add(new CasioGWB5600DeviceCoordinator());
|
||||||
result.add(new BFH16DeviceCoordinator());
|
result.add(new BFH16DeviceCoordinator());
|
||||||
result.add(new MijiaLywsd02Coordinator());
|
result.add(new MijiaLywsd02Coordinator());
|
||||||
result.add(new ITagCoordinator());
|
result.add(new ITagCoordinator());
|
||||||
|
@ -1291,6 +1291,7 @@
|
|||||||
<string name="devicetype_y5">Y5</string>
|
<string name="devicetype_y5">Y5</string>
|
||||||
<string name="devicetype_casiogb6900">Casio GB-6900</string>
|
<string name="devicetype_casiogb6900">Casio GB-6900</string>
|
||||||
<string name="devicetype_casiogbx100">Casio GBX-100</string>
|
<string name="devicetype_casiogbx100">Casio GBX-100</string>
|
||||||
|
<string name="devicetype_casiogwb5600">Casio GW-B5600</string>
|
||||||
<string name="devicetype_miscale2">Mi Scale 2</string>
|
<string name="devicetype_miscale2">Mi Scale 2</string>
|
||||||
<string name="devicetype_itag">iTag</string>
|
<string name="devicetype_itag">iTag</string>
|
||||||
<string name="devicetype_bfh16">BFH-16</string>
|
<string name="devicetype_bfh16">BFH-16</string>
|
||||||
@ -1523,7 +1524,7 @@
|
|||||||
<string name="about_contributors">Contributors</string>
|
<string name="about_contributors">Contributors</string>
|
||||||
<string name="about_core_team_members" translatable="false">Andreas Shimokawa\nCarsten Pfeiffer\nDaniele Gobbetti</string>
|
<string name="about_core_team_members" translatable="false">Andreas Shimokawa\nCarsten Pfeiffer\nDaniele Gobbetti</string>
|
||||||
<string name="about_additional_device_support">Additional device support</string>
|
<string name="about_additional_device_support">Additional device support</string>
|
||||||
<string name="about_additional_contributors" translatable="false">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)</string>
|
<string name="about_additional_contributors" translatable="false">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)</string>
|
||||||
<string name="about_additional_contributions">Many thanks to all unlisted contributors for contributing code, translations, support, ideas, motivation, bug reports, money… ✊</string>
|
<string name="about_additional_contributions">Many thanks to all unlisted contributors for contributing code, translations, support, ideas, motivation, bug reports, money… ✊</string>
|
||||||
<string name="about_links">Links</string>
|
<string name="about_links">Links</string>
|
||||||
<string name="permission_granting_mandatory">All these permissions are required and instability might occur if not granted</string>
|
<string name="permission_granting_mandatory">All these permissions are required and instability might occur if not granted</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user