From 9454684974bf4add544351adf33d41d7d7f02fdf Mon Sep 17 00:00:00 2001 From: Yukai Li Date: Sun, 4 Oct 2020 21:00:56 -0600 Subject: [PATCH] Lefun: Add device support and initialization code --- .../devices/lefun/LefunConstants.java | 2 + .../service/DeviceSupportFactory.java | 4 + .../devices/lefun/LefunDeviceSupport.java | 297 ++++++++++++++++++ .../requests/GetBatteryLevelRequest.java | 52 +++ .../requests/GetFirmwareInfoRequest.java | 57 ++++ .../devices/lefun/requests/Request.java | 74 +++++ .../lefun/requests/SetTimeRequest.java | 63 ++++ 7 files changed, 549 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetBatteryLevelRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetFirmwareInfoRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/Request.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetTimeRequest.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java index c184fc260..a4b972634 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java @@ -25,6 +25,8 @@ import static nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDevi public class LefunConstants { // BLE UUIDs public static final UUID UUID_SERVICE_LEFUN = UUID.fromString(String.format(BASE_UUID, "18D0")); + public static final UUID UUID_CHARACTERISTIC_LEFUN_WRITE = UUID.fromString(String.format(BASE_UUID, "2D01")); + public static final UUID UUID_CHARACTERISTIC_LEFUN_NOTIFY = UUID.fromString(String.format(BASE_UUID, "2D00")); // Coordinator constants public static final String ADVERTISEMENT_NAME = "Lefun"; 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 e158a94a6..44b1b3ef4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.id115.ID115Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.itag.ITagSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.BFH16DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30.TeclastH30Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.makibeshr3.MakibesHR3DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; @@ -260,6 +261,9 @@ public class DeviceSupportFactory { case SG2: deviceSupport = new ServiceDeviceSupport(new HPlusSupport(DeviceType.SG2), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case LEFUN: + deviceSupport = new ServiceDeviceSupport(new LefunDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java new file mode 100644 index 000000000..27292b93d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java @@ -0,0 +1,297 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + 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.lefun; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.net.Uri; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetBatteryLevelRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetFirmwareInfoRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.Request; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetTimeRequest; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class LefunDeviceSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(LefunDeviceSupport.class); + + private final List inProgressRequests = Collections.synchronizedList(new ArrayList()); + + public LefunDeviceSupport() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); + addSupportedService(LefunConstants.UUID_SERVICE_LEFUN); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.setGattCallback(this); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + + // Enable notification + builder.notify(getCharacteristic(LefunConstants.UUID_CHARACTERISTIC_LEFUN_NOTIFY), true); + + // Init device (just get version and battery) + try { + GetFirmwareInfoRequest firmwareReq = new GetFirmwareInfoRequest(this, builder); + firmwareReq.perform(); + inProgressRequests.add(firmwareReq); + + SetTimeRequest timeReq = new SetTimeRequest(this, builder); + timeReq.perform(); + inProgressRequests.add(timeReq); + + GetBatteryLevelRequest batReq = new GetBatteryLevelRequest(this, builder); + batReq.perform(); + inProgressRequests.add(batReq); + } catch (IOException e) { + GB.toast(getContext(), "Failed to initialize Lefun device", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + + return builder; + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onSetTime() { + + } + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onSetCallState(CallSpec callSpec) { + + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config, Integer id) { + + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onFetchRecordedData(int dataTypes) { + + } + + @Override + public void onReset(int flags) { + + } + + @Override + public void onHeartRateTest() { + + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onReadConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (characteristic.getUuid().equals(LefunConstants.UUID_CHARACTERISTIC_LEFUN_NOTIFY)) { + byte[] data = characteristic.getValue(); + // Parse response + if (data.length >= LefunConstants.CMD_HEADER_LENGTH && data[0] == LefunConstants.CMD_RESPONSE_ID) { + // Note: full validation is done within the request + byte commandId = data[2]; + synchronized (inProgressRequests) { + for (Request req : inProgressRequests) { + if (req.expectsResponse() && req.getCommandId() == commandId) { + try { + req.handleResponse(data); + inProgressRequests.remove(req); + return true; + } catch (IllegalArgumentException e) { + LOG.error("Failed to handle response", e); + } + } + } + } + + if (handleAsynchronousResponse(data)) + return true; + + LOG.error(String.format("No handler for response 0x%02x", commandId)); + return false; + } + + LOG.error("Invalid response received"); + return false; + } + + return super.onCharacteristicChanged(gatt, characteristic); + } + + private boolean handleAsynchronousResponse(byte[] data) { + // Assume data already checked for correct response code and length + return false; + } + + public void completeInitialization() { + gbDevice.setState(GBDevice.State.INITIALIZED); + gbDevice.sendDeviceUpdateIntent(getContext()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetBatteryLevelRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetBatteryLevelRequest.java new file mode 100644 index 000000000..35964c120 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetBatteryLevelRequest.java @@ -0,0 +1,52 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + 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.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetBatteryLevelCommand; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class GetBatteryLevelRequest extends Request { + public GetBatteryLevelRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + GetBatteryLevelCommand cmd = new GetBatteryLevelCommand(); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetBatteryLevelCommand cmd = new GetBatteryLevelCommand(); + cmd.deserialize(data); + + GBDevice device = getSupport().getDevice(); + device.setBatteryLevel(cmd.getBatteryLevel()); + device.setBatteryThresholdPercent((short)15); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_BATTERY_LEVEL; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetFirmwareInfoRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetFirmwareInfoRequest.java new file mode 100644 index 000000000..f3c900a12 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetFirmwareInfoRequest.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + 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.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetFirmwareInfoCommand; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class GetFirmwareInfoRequest extends Request { + public GetFirmwareInfoRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + GetFirmwareInfoCommand cmd = new GetFirmwareInfoCommand(); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetFirmwareInfoCommand cmd = new GetFirmwareInfoCommand(); + cmd.deserialize(data); + + GBDevice device = getSupport().getDevice(); + // Last character is a \x1f? Not printable either way. + device.setModel(cmd.getTypeCode().substring(0, 3)); + int hardwareVersion = cmd.getHardwareVersion() & 0xffff; + int softwareVersion = cmd.getSoftwareVersion() & 0xffff; + device.setFirmwareVersion(String.format("%d.%d", softwareVersion >> 8, softwareVersion & 0xff)); + device.setFirmwareVersion2(String.format("%d.%d", hardwareVersion >> 8, hardwareVersion & 0xff)); + getSupport().completeInitialization(); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_FIRMWARE_INFO; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/Request.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/Request.java new file mode 100644 index 000000000..6121fd30c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/Request.java @@ -0,0 +1,74 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + 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.lefun.requests; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +// Ripped from nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request +public abstract class Request extends AbstractBTLEOperation { + private Logger logger = (Logger) LoggerFactory.getLogger(getName()); + protected TransactionBuilder builder; + + protected Request(LefunDeviceSupport support, TransactionBuilder builder) { + super(support); + this.builder = builder; + } + + @Override + protected void doPerform() throws IOException { + BluetoothGattCharacteristic characteristic = getSupport() + .getCharacteristic(LefunConstants.UUID_CHARACTERISTIC_LEFUN_WRITE); + builder.write(characteristic, createRequest()); + } + + public abstract byte[] createRequest(); + + public void handleResponse(byte[] data) { + } + + public String getName() { + Class thisClass = getClass(); + while (thisClass.isAnonymousClass()) thisClass = thisClass.getSuperclass(); + return thisClass.getSimpleName(); + } + + protected void log(String message) { + logger.debug(message); + } + + public abstract int getCommandId(); + + public boolean expectsResponse() { + return true; + } + + protected void reportFailure(String message) { + // TODO: Toast here or something + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetTimeRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetTimeRequest.java new file mode 100644 index 000000000..ac6f47940 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetTimeRequest.java @@ -0,0 +1,63 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + 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.lefun.requests; + +import java.util.Calendar; +import java.util.TimeZone; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.TimeCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class SetTimeRequest extends Request { + public SetTimeRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + TimeCommand cmd = new TimeCommand(); + Calendar c = Calendar.getInstance(); + + cmd.setOp(BaseCommand.OP_SET); + cmd.setYear((byte)(c.get(Calendar.YEAR) - 2000)); + cmd.setMonth((byte)c.get(Calendar.MONTH)); + cmd.setDay((byte)c.get(Calendar.DAY_OF_MONTH)); + cmd.setHour((byte)c.get(Calendar.HOUR_OF_DAY)); + cmd.setMinute((byte)c.get(Calendar.MINUTE)); + cmd.setSecond((byte)c.get(Calendar.SECOND)); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + TimeCommand cmd = new TimeCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_SET && !cmd.isSetSuccess()) + reportFailure("Could not set time"); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_TIME; + } +}