Casio ECB-S100 support

Lightly refactoring gwb5600 as a starting base, since the commands supported etc seem pretty similar. Using as well https://github.com/izivkov/GShockAPI as further reference for what is supported (which I tested with the watch) and the same commands.
This commit is contained in:
Jisakiel 2024-10-01 21:34:49 +01:00
parent bac9c8b239
commit 960a864ea0
9 changed files with 280 additions and 106 deletions

View File

@ -95,7 +95,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
private Pattern supportedDeviceName = null;
/**
* This method should return a ReGexp pattern that will matched against a found device
* This method should return a Regexp pattern that will matched against a found device
* to check whether this coordinator supports that device.
* If more sophisticated logic is needed to determine device support, the supports(GBDeviceCandidate)
* should be overridden.

View File

@ -0,0 +1,67 @@
package nodomain.freeyourgadget.gadgetbridge.devices.casio.ecbs100;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.Casio2C2DDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.ecbs100.CasioECBS100DeviceSupport;
public class CasioECBS100DeviceCoordinator extends Casio2C2DDeviceCoordinator {
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_BOND;
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_timeformat,
R.xml.devicesettings_dateformat_day_month_order,
R.xml.devicesettings_operating_sounds,
R.xml.devicesettings_hourly_chime_enable,
R.xml.devicesettings_autolight,
R.xml.devicesettings_light_duration_longer,
R.xml.devicesettings_power_saving,
R.xml.devicesettings_casio_connection_duration,
R.xml.devicesettings_time_sync,
// timer
// reminder
// world time
};
}
@Override
public int getAlarmSlotCount(GBDevice device) {
return 5;
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return CasioECBS100DeviceSupport.class;
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("CASIO ECB-S100");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_casioecbs100;
}
}

View File

@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.asteroidos.AsteroidOSDeviceC
import nodomain.freeyourgadget.gadgetbridge.devices.bandwpseries.BandWPSeriesDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.binary_sensor.coordinator.BinarySensorCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.ecbs100.CasioECBS100DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gb6900.CasioGB6900DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gbx100.CasioGBX100DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGMWB5000DeviceCoordinator;
@ -405,6 +406,8 @@ public enum DeviceType {
WATCHXPLUS(WatchXPlusDeviceCoordinator.class),
ROIDMI(Roidmi1Coordinator.class),
ROIDMI3(Roidmi3Coordinator.class),
CASIOECBS100(CasioECBS100DeviceCoordinator.class),
CASIOGB6900(CasioGB6900DeviceCoordinator.class),
CASIOGBX100(CasioGBX100DeviceCoordinator.class),
CASIOGWB5600(CasioGWB5600DeviceCoordinator.class),

View File

@ -0,0 +1,114 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import org.slf4j.Logger;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.TextStyle;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
public abstract class BasicCasio2C2DSupport extends Casio2C2DSupport {
public BasicCasio2C2DSupport(Logger logger) {
super(logger);
}
@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;
}
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
super.initializeDevice(builder);
// which button was pressed?
requestFeature(builder, new FeatureRequest(FEATURE_BLE_FEATURES), data -> {
if (data.length > 8 && data[8] == CasioConstants.CONNECT_FIND) {
setInitialized();
} else {
requestWorldClocks();
}
});
return builder;
}
protected void requestWorldClocks() {
TransactionBuilder builder = createTransactionBuilder("requestWorldClocks");
HashSet<FeatureRequest> requests = new HashSet();
for (byte i = 0; i < 6; i++) {
requests.addAll(CasioTimeZone.requests(i));
}
requestFeatures(builder, requests, responses -> {
TransactionBuilder clockBuilder = createTransactionBuilder("setClocks");
setClocks(clockBuilder, responses);
clockBuilder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
clockBuilder.queue(getQueue());
});
builder.queue(getQueue());
}
private void setClocks(TransactionBuilder builder, Map<FeatureRequest, byte[]> responses) {
ZoneId tz = ZoneId.systemDefault();
Instant now = Instant.now().plusSeconds(2);
CasioTimeZone[] timezones = {
CasioTimeZone.fromZoneId(tz, now, tz.getDisplayName(TextStyle.SHORT, Locale.getDefault())),
CasioTimeZone.fromWatchResponses(responses, 1),
CasioTimeZone.fromWatchResponses(responses, 2),
CasioTimeZone.fromWatchResponses(responses, 3),
CasioTimeZone.fromWatchResponses(responses, 4),
CasioTimeZone.fromWatchResponses(responses, 5),
};
for (int i = 5; i >= 0; i--) {
if (i%2 == 0)
writeAllFeatures(builder, CasioTimeZone.dstWatchStateBytes(i, timezones[i], i+1, timezones[i+1]));
writeAllFeatures(builder, timezones[i].dstSettingBytes(i));
writeAllFeatures(builder, timezones[i].worldCityBytes(i));
}
writeCurrentTime(builder, ZonedDateTime.ofInstant(now, tz));
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
UUID characteristicUUID = characteristic.getUuid();
byte[] data = characteristic.getValue();
if (data.length == 0)
return true;
if (characteristicUUID.equals(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID)) {
if(data[0] == FEATURE_ALERT_LEVEL) {
GBDeviceEventFindPhone event = new GBDeviceEventFindPhone();
if(data[1] == 0x02) {
event.event = GBDeviceEventFindPhone.Event.START_VIBRATE;
} else {
event.event = GBDeviceEventFindPhone.Event.STOP;
}
evaluateGBDeviceEvent(event);
return true;
}
}
return super.onCharacteristicChanged(gatt, characteristic);
}
}

View File

@ -110,7 +110,7 @@ public abstract class Casio2C2DSupport extends CasioSupport {
public static final byte FEATURE_SETTING_FOR_USER_PROFILE = 0x45;
public static final byte FEATURE_SERVICE_DISCOVERY_MANAGER = 0x47;
private static Logger LOG;
protected static Logger LOG;
LinkedList<RequestWithHandler> requests = new LinkedList<>();
public Casio2C2DSupport(Logger logger) {

View File

@ -15,7 +15,7 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600;
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio;
import java.util.Set;
import java.util.HashSet;
@ -31,16 +31,12 @@ import java.time.ZoneOffset;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneOffsetTransitionRule;
import java.time.zone.ZoneRules;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.Casio2C2DSupport;
public class CasioGWB5600TimeZone {
public class CasioTimeZone {
/*
There are six clocks on the Casio GW-B5600
There are six clocks on the Casio GW-B5600 / S100
0 is the main clock
1-5 are the world clocks
@ -123,7 +119,7 @@ PONTA DELGADA E4 00 FC 04 02
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) {
private CasioTimeZone(byte[] name, byte[] number, byte offset, byte dstOffset, byte dstRules, byte dstSetting) {
this.name = name;
this.number = number;
this.offset = offset;
@ -140,7 +136,7 @@ PONTA DELGADA E4 00 FC 04 02
return requests;
}
static public byte[] dstWatchStateBytes(int slotA, CasioGWB5600TimeZone zoneA, int slotB, CasioGWB5600TimeZone zoneB) {
static public byte[] dstWatchStateBytes(int slotA, CasioTimeZone zoneA, int slotB, CasioTimeZone zoneB) {
return new byte[] {
Casio2C2DSupport.FEATURE_DST_WATCH_STATE,
(byte) slotA,
@ -174,7 +170,7 @@ PONTA DELGADA E4 00 FC 04 02
return bytes;
}
static CasioGWB5600TimeZone fromWatchResponses(Map<Casio2C2DSupport.FeatureRequest, byte[]> responses, int slot) {
public static CasioTimeZone fromWatchResponses(Map<Casio2C2DSupport.FeatureRequest, byte[]> responses, int slot) {
byte[] name = "unknown".getBytes(StandardCharsets.US_ASCII);
byte[] number = {0,0};
byte offset = 0;
@ -208,10 +204,10 @@ PONTA DELGADA E4 00 FC 04 02
}
}
return new CasioGWB5600TimeZone(name, number, offset, dstOffset, dstRules, dstSetting);
return new CasioTimeZone(name, number, offset, dstOffset, dstRules, dstSetting);
}
static CasioGWB5600TimeZone fromZoneId(ZoneId zone, Instant time, String zoneName) {
public static CasioTimeZone fromZoneId(ZoneId zone, Instant time, String zoneName) {
ZoneRules rules = zone.getRules();
byte[] name = zoneName.getBytes(StandardCharsets.US_ASCII);
@ -249,7 +245,7 @@ PONTA DELGADA E4 00 FC 04 02
}
}
return new CasioGWB5600TimeZone(name, number, offset, dstOffset, dstRules, dstSetting);
return new CasioTimeZone(name, number, offset, dstOffset, dstRules, dstSetting);
}
// We are searching for watch DST rules which match the next two transitions.

View File

@ -0,0 +1,82 @@
/* Copyright (C) 2023-2024 Johannes Krude, Jisakiel
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio.ecbs100;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.TextStyle;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.BasicCasio2C2DSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.Casio2C2DSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.CasioTimeZone;
// TODO abstract methods from GWB5600
public class CasioECBS100DeviceSupport extends BasicCasio2C2DSupport {
private static final Logger LOG = LoggerFactory.getLogger(CasioECBS100DeviceSupport.class);
public CasioECBS100DeviceSupport() {
super(LOG);
}
@Override
public DevicePreference[] supportedDevicePreferences() {
return new DevicePreference[] {
new LanguagePreference(),
new TimeFormatPreference(),
new DayMonthOrderPreference(),
new OperatingSoundPreference(),
new HourlyChimePreference(),
new AutoLightPreference(),
new LongerLightDurationPreference(),
new PowerSavingPreference(),
new ConnectionDurationPreference(),
new TimeSyncPreference(),
};
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
public boolean connectFirstTime() {
// remove this workaround in case Gadgetbridge fixes
// https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/3216
setAutoReconnect(true);
return connect();
}
}

View File

@ -32,7 +32,6 @@ import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.TextStyle;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
@ -40,9 +39,9 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.Casio2C2DSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.gwb5600.CasioGWB5600TimeZone;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.CasioTimeZone;
public class CasioGWB5600DeviceSupport extends Casio2C2DSupport {
public class CasioGWB5600DeviceSupport extends BasicCasio2C2DSupport {
private static final Logger LOG = LoggerFactory.getLogger(CasioGWB5600DeviceSupport.class);
public CasioGWB5600DeviceSupport() {
@ -77,92 +76,4 @@ public class CasioGWB5600DeviceSupport extends Casio2C2DSupport {
setAutoReconnect(true);
return connect();
}
@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;
}
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
super.initializeDevice(builder);
// which button was pressed?
requestFeature(builder, new FeatureRequest(FEATURE_BLE_FEATURES), data -> {
if (data.length > 8 && data[8] == CasioConstants.CONNECT_FIND) {
setInitialized();
} else {
requestWorldClocks();
}
});
return builder;
}
private void requestWorldClocks() {
TransactionBuilder builder = createTransactionBuilder("requestWorldClocks");
HashSet<FeatureRequest> requests = new HashSet();
for (byte i = 0; i < 6; i++) {
requests.addAll(CasioGWB5600TimeZone.requests(i));
}
requestFeatures(builder, requests, responses -> {
TransactionBuilder clockBuilder = createTransactionBuilder("setClocks");
setClocks(clockBuilder, responses);
clockBuilder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
clockBuilder.queue(getQueue());
});
builder.queue(getQueue());
}
private void setClocks(TransactionBuilder builder, Map<FeatureRequest, byte[]> responses) {
ZoneId tz = ZoneId.systemDefault();
Instant now = Instant.now().plusSeconds(2);
CasioGWB5600TimeZone[] timezones = {
CasioGWB5600TimeZone.fromZoneId(tz, now, tz.getDisplayName(TextStyle.SHORT, Locale.getDefault())),
CasioGWB5600TimeZone.fromWatchResponses(responses, 1),
CasioGWB5600TimeZone.fromWatchResponses(responses, 2),
CasioGWB5600TimeZone.fromWatchResponses(responses, 3),
CasioGWB5600TimeZone.fromWatchResponses(responses, 4),
CasioGWB5600TimeZone.fromWatchResponses(responses, 5),
};
for (int i = 5; i >= 0; i--) {
if (i%2 == 0)
writeAllFeatures(builder, CasioGWB5600TimeZone.dstWatchStateBytes(i, timezones[i], i+1, timezones[i+1]));
writeAllFeatures(builder, timezones[i].dstSettingBytes(i));
writeAllFeatures(builder, timezones[i].worldCityBytes(i));
}
writeCurrentTime(builder, ZonedDateTime.ofInstant(now, tz));
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
UUID characteristicUUID = characteristic.getUuid();
byte[] data = characteristic.getValue();
if (data.length == 0)
return true;
if (characteristicUUID.equals(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID)) {
if(data[0] == FEATURE_ALERT_LEVEL) {
GBDeviceEventFindPhone event = new GBDeviceEventFindPhone();
if(data[1] == 0x02) {
event.event = GBDeviceEventFindPhone.Event.START_VIBRATE;
} else {
event.event = GBDeviceEventFindPhone.Event.STOP;
}
evaluateGBDeviceEvent(event);
return true;
}
}
return super.onCharacteristicChanged(gatt, characteristic);
}
}

View File

@ -1807,6 +1807,7 @@
<string name="devicetype_roidmi">Roidmi</string>
<string name="devicetype_roidmi3">Roidmi 3</string>
<string name="devicetype_y5">Y5</string>
<string name="devicetype_casioecbs100">Casio ECB-S100</string>
<string name="devicetype_casiogb6900">Casio GB-6900</string>
<string name="devicetype_casiogbx100">Casio GBX-100</string>
<string name="devicetype_casiogwb5600">Casio GW-B5600</string>