From cc6c57bd4c127900a3034a656d9ff5c7b95ba4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 28 Jul 2018 16:23:58 +0100 Subject: [PATCH 1/2] Add support for LED Color, FM Frequency --- app/build.gradle | 2 +- .../adapter/GBDeviceAdapterv2.java | 116 +++++++++++++++++- .../GBDeviceEventBatteryInfo.java | 1 + .../GBDeviceEventFmFrequency.java | 21 ++++ .../deviceevents/GBDeviceEventLEDColor.java | 21 ++++ .../devices/AbstractDeviceCoordinator.java | 14 +++ .../devices/DeviceCoordinator.java | 17 +++ .../gadgetbridge/devices/EventHandler.java | 8 ++ .../devices/UnknownDeviceCoordinator.java | 15 +++ .../gadgetbridge/impl/GBDevice.java | 49 ++++++++ .../gadgetbridge/impl/GBDeviceService.java | 14 +++ .../gadgetbridge/model/BatteryState.java | 3 +- .../gadgetbridge/model/DeviceService.java | 4 + .../service/AbstractDeviceSupport.java | 27 ++++ .../service/DeviceCommunicationService.java | 16 +++ .../service/ServiceDeviceSupport.java | 16 +++ .../btle/AbstractBTLEDeviceSupport.java | 10 ++ .../serial/AbstractSerialDeviceSupport.java | 12 ++ .../service/serial/GBDeviceProtocol.java | 8 ++ app/src/main/res/drawable/ic_led_color.xml | 11 ++ app/src/main/res/drawable/ic_radio.xml | 9 ++ app/src/main/res/layout/device_itemv2.xml | 47 ++++++- app/src/main/res/values-pt/strings.xml | 12 ++ app/src/main/res/values/strings.xml | 10 ++ .../service/TestDeviceSupport.java | 10 ++ 25 files changed, 469 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventFmFrequency.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java create mode 100644 app/src/main/res/drawable/ic_led_color.xml create mode 100644 app/src/main/res/drawable/ic_radio.xml diff --git a/app/build.gradle b/app/build.gradle index cc97804f6..0392369ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,7 @@ dependencies { implementation "org.greenrobot:greendao:2.2.1" implementation "org.apache.commons:commons-lang3:3.5" implementation "org.cyanogenmod:platform.sdk:6.0" - + implementation 'com.jaredrummler:colorpicker:1.0.2' // implementation project(":DaoCore") } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java index ed9fdd9a6..e9b057f05 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java @@ -22,16 +22,19 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.graphics.drawable.GradientDrawable; import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; +import android.text.InputType; import android.transition.TransitionManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; +import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; @@ -40,7 +43,11 @@ import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; +import com.jaredrummler.android.colorpicker.ColorPickerDialog; +import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; + import java.util.List; +import java.util.Locale; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; @@ -125,16 +132,22 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter 108.0) { + new AlertDialog.Builder(context) + .setTitle(R.string.pref_invalid_frequency_title) + .setMessage(R.string.pref_invalid_frequency_message) + .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + } + }) + .show(); + } else { + device.setExtraInfo("fm_frequency", frequency); + fmFrequencyLabel.setText(String.format(Locale.getDefault(), "%.1f", (float) device.getExtraInfo("fm_frequency"))); + GBApplication.deviceService().onSetFmFrequency(frequency); + } + } + }); + builder.setNegativeButton(context.getResources().getString(R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + }); + + holder.ledColor.setVisibility(View.GONE); + if (device.isInitialized() && device.getExtraInfo("led_color") != null && coordinator.supportsLedColor()) { + holder.ledColor.setVisibility(View.VISIBLE); + final GradientDrawable ledColor = (GradientDrawable) holder.ledColor.getDrawable().mutate(); + ledColor.setColor((int) device.getExtraInfo("led_color")); + holder.ledColor.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ColorPickerDialog.Builder builder = ColorPickerDialog.newBuilder(); + builder.setDialogTitle(R.string.preferences_led_color); + + builder.setColor((int) device.getExtraInfo("led_color")); + if (coordinator.supportsRgbLedColor()) { + builder.setAllowCustom(true); + builder.setShowAlphaSlider(false); + builder.setAllowPresets(true); + } else { + builder.setAllowCustom(false); + builder.setAllowPresets(true); + builder.setShowColorShades(false); + builder.setPresets(coordinator.getColorPresets()); + } + + ColorPickerDialog dialog = builder.create(); + dialog.setColorPickerDialogListener(new ColorPickerDialogListener() { + @Override + public void onColorSelected(int dialogId, int color) { + ledColor.setColor(color); + device.setExtraInfo("led_color", color); + GBApplication.deviceService().onSetLedColor(color); + } + + @Override + public void onDialogDismissed(int dialogId) { + // Nothing to do + } + }); + dialog.show(((Activity) context).getFragmentManager(), "color-picker-dialog"); + } + }); + } + //remove device, hidden under details holder.removeDevice.setOnClickListener(new View.OnClickListener() @@ -373,6 +481,9 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter. */ +package nodomain.freeyourgadget.gadgetbridge.deviceevents; + +public class GBDeviceEventFmFrequency extends GBDeviceEvent { + public float frequency; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java new file mode 100644 index 000000000..a3c7ca807 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java @@ -0,0 +1,21 @@ +/* Copyright (C) 2018 José Rebelo + + 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.deviceevents; + +public class GBDeviceEventLEDColor extends GBDeviceEvent { + public int color; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index de79565c7..d6b6795f7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -135,4 +135,18 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { public boolean supportsMusicInfo() { return false; } + + public boolean supportsLedColor() { + return false; + } + + @Override + public boolean supportsRgbLedColor() { + return false; + } + + @Override + public int[] getColorPresets() { + return new int[0]; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 859db7939..5364526eb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -254,4 +254,21 @@ public interface DeviceCoordinator { * like artist, title, album, play state etc. */ boolean supportsMusicInfo(); + + /** + * Indicates whether the device has an led which supports custom colors + */ + boolean supportsLedColor(); + + /** + * Indicates whether the device's led supports any RGB color, + * or only preset colors + */ + boolean supportsRgbLedColor(); + + /** + * Returns the preset colors supported by the device, if any, in ARGB, with alpha = 255 + */ + @NonNull + int[] getColorPresets(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java index 4acf58aea..5e1fad92a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java @@ -99,4 +99,12 @@ public interface EventHandler { void onTestNewFunction(); void onSendWeather(WeatherSpec weatherSpec); + + void onSetFmFrequency(float frequency); + + /** + * Set the device's led color. + * @param color the new color, in ARGB, with alpha = 255 + */ + void onSetLedColor(int color); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java index c06455faa..6a030fa4a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java @@ -186,4 +186,19 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator { public boolean supportsFindDevice() { return false; } + + @Override + public boolean supportsLedColor() { + return false; + } + + @Override + public boolean supportsRgbLedColor() { + return false; + } + + @Override + public int[] getColorPresets() { + return new int[0]; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java index 296ae7bb2..2adc2ae71 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -73,11 +74,13 @@ public class GBDevice implements Parcelable { private String mModel; private State mState = State.NOT_CONNECTED; private short mBatteryLevel = BATTERY_UNKNOWN; + private float mBatteryVoltage = BATTERY_UNKNOWN; private short mBatteryThresholdPercent = BATTERY_THRESHOLD_PERCENT; private BatteryState mBatteryState; private short mRssi = RSSI_UNKNOWN; private String mBusyTask; private List mDeviceInfos; + private HashMap mExtraInfos; public GBDevice(String address, String name, DeviceType deviceType) { this(address, null, name, deviceType); @@ -106,6 +109,7 @@ public class GBDevice implements Parcelable { mRssi = (short) in.readInt(); mBusyTask = in.readString(); mDeviceInfos = in.readArrayList(getClass().getClassLoader()); + mExtraInfos = (HashMap) in.readSerializable(); validate(); } @@ -126,6 +130,7 @@ public class GBDevice implements Parcelable { dest.writeInt(mRssi); dest.writeString(mBusyTask); dest.writeList(mDeviceInfos); + dest.writeSerializable(mExtraInfos); } private void validate() { @@ -371,6 +376,33 @@ public class GBDevice implements Parcelable { return mAddress.hashCode() ^ 37; } + + /** + * Returns the extra info value if it is set, null otherwise + * @param key the extra info key + * @return the extra info value if set, null otherwise + */ + public Object getExtraInfo(String key) { + if (mExtraInfos == null) { + return null; + } + + return mExtraInfos.get(key); + } + + /** + * Sets an extra info value, overwriting the current one, if any + * @param key the extra info key + * @param info the extra info value + */ + public void setExtraInfo(String key, Object info) { + if (mExtraInfos == null) { + mExtraInfos = new HashMap<>(); + } + + mExtraInfos.put(key, info); + } + /** * Ranges from 0-100 (percent), or -1 if unknown * @@ -388,6 +420,23 @@ public class GBDevice implements Parcelable { } } + public void setBatteryVoltage(float batteryVoltage) { + if (batteryVoltage >= 0 || batteryVoltage == BATTERY_UNKNOWN) { + mBatteryVoltage = batteryVoltage; + } else { + LOG.error("Battery voltage must be > 0: " + batteryVoltage); + } + } + + /** + * Voltage greater than zero (unit: Volt), or -1 if unknown + * + * @return the battery voltage, or -1 if unknown + */ + public float getBatteryVoltage() { + return mBatteryVoltage; + } + public BatteryState getBatteryState() { return mBatteryState; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index 478650f03..8a997a997 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -405,4 +405,18 @@ public class GBDeviceService implements DeviceService { return name; } + + @Override + public void onSetFmFrequency(float frequency) { + Intent intent = createIntent().setAction(ACTION_SET_FM_FREQUENCY) + .putExtra(EXTRA_FM_FREQUENCY, frequency); + invokeService(intent); + } + + @Override + public void onSetLedColor(int color) { + Intent intent = createIntent().setAction(ACTION_SET_LED_COLOR) + .putExtra(EXTRA_LED_COLOR, color); + invokeService(intent); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java index 346612a43..25538d675 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java @@ -22,5 +22,6 @@ public enum BatteryState { BATTERY_LOW, BATTERY_CHARGING, BATTERY_CHARGING_FULL, - BATTERY_NOT_CHARGING_FULL + BATTERY_NOT_CHARGING_FULL, + NO_BATTERY } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index 92b359701..d15e858db 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -65,6 +65,8 @@ public interface DeviceService extends EventHandler { String ACTION_SEND_CONFIGURATION = PREFIX + ".action.send_configuration"; String ACTION_SEND_WEATHER = PREFIX + ".action.send_weather"; String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function"; + String ACTION_SET_FM_FREQUENCY = PREFIX + ".action.set_fm_frequency"; + String ACTION_SET_LED_COLOR = PREFIX + ".action.set_led_color"; String EXTRA_NOTIFICATION_BODY = "notification_body"; String EXTRA_NOTIFICATION_FLAGS = "notification_flags"; String EXTRA_NOTIFICATION_ID = "notification_id"; @@ -106,6 +108,8 @@ public interface DeviceService extends EventHandler { String EXTRA_INTERVAL_SECONDS = "interval_seconds"; String EXTRA_WEATHER = "weather"; String EXTRA_RECORDED_DATA_TYPES = "data_types"; + String EXTRA_FM_FREQUENCY = "fm_frequency"; + String EXTRA_LED_COLOR = "led_color"; /** * Use EXTRA_REALTIME_SAMPLE instead diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index 92d011471..a06bc27d4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -52,7 +52,9 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; @@ -159,6 +161,10 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { handleGBDeviceEvent((GBDeviceEventBatteryInfo) deviceEvent); } else if (deviceEvent instanceof GBDeviceEventFindPhone) { handleGBDeviceEvent((GBDeviceEventFindPhone) deviceEvent); + } else if (deviceEvent instanceof GBDeviceEventLEDColor) { + handleGBDeviceEvent((GBDeviceEventLEDColor) deviceEvent); + } else if (deviceEvent instanceof GBDeviceEventFmFrequency) { + handleGBDeviceEvent((GBDeviceEventFmFrequency) deviceEvent); } } @@ -208,6 +214,26 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { gbDevice.sendDeviceUpdateIntent(context); } + protected void handleGBDeviceEvent(GBDeviceEventLEDColor colorEvent) { + Context context = getContext(); + LOG.info("Got event for LED Color"); + if (gbDevice == null) { + return; + } + gbDevice.setExtraInfo("led_color", colorEvent.color); + gbDevice.sendDeviceUpdateIntent(context); + } + + protected void handleGBDeviceEvent(GBDeviceEventFmFrequency frequencyEvent) { + Context context = getContext(); + LOG.info("Got event for FM Frequency"); + if (gbDevice == null) { + return; + } + gbDevice.setExtraInfo("fm_frequency", frequencyEvent.frequency); + gbDevice.sendDeviceUpdateIntent(context); + } + private void handleGBDeviceEvent(GBDeviceEventAppInfo appInfoEvent) { Context context = getContext(); LOG.info("Got event for APP_INFO"); @@ -328,6 +354,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { LOG.info("Got BATTERY_INFO device event"); gbDevice.setBatteryLevel(deviceEvent.level); gbDevice.setBatteryState(deviceEvent.state); + gbDevice.setBatteryVoltage(deviceEvent.voltage); //show the notification if the battery level is below threshold and only if not connected to charger if (deviceEvent.level <= gbDevice.getBatteryThresholdPercent() && diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index b050177a6..2c183defb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -105,7 +105,9 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETTIME; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_ALARMS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_CONSTANT_VIBRATION; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_FM_FREQUENCY; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_HEARTRATE_MEASUREMENT_INTERVAL; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_LED_COLOR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_TEST_NEW_FUNCTION; @@ -130,7 +132,9 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAN import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONNECT_FIRST_TIME; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FM_FREQUENCY; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_INTERVAL_SECONDS; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_LED_COLOR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_DURATION; @@ -549,6 +553,18 @@ public class DeviceCommunicationService extends Service implements SharedPrefere } break; } + case ACTION_SET_LED_COLOR: + int color = intent.getIntExtra(EXTRA_LED_COLOR, 0); + if (color != 0) { + mDeviceSupport.onSetLedColor(color); + } + break; + case ACTION_SET_FM_FREQUENCY: + float frequency = intent.getFloatExtra(EXTRA_FM_FREQUENCY, -1); + if (frequency != -1) { + mDeviceSupport.onSetFmFrequency(frequency); + } + break; } return START_STICKY; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index 4c77c5a01..33e49e48a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -374,4 +374,20 @@ public class ServiceDeviceSupport implements DeviceSupport { } delegate.onSendWeather(weatherSpec); } + + @Override + public void onSetFmFrequency(float frequency) { + if (checkBusy("set frequency event")) { + return; + } + delegate.onSetFmFrequency(frequency); + } + + @Override + public void onSetLedColor(int color) { + if (checkBusy("set led color event")) { + return; + } + delegate.onSetLedColor(color); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index e7f157dfe..c65153ba0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -318,4 +318,14 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im profile.onReadRemoteRssi(gatt, rssi, status); } } + + @Override + public void onSetFmFrequency(float frequency) { + + } + + @Override + public void onSetLedColor(int color) { + + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java index e73527eab..c9050af91 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java @@ -250,4 +250,16 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport byte[] bytes = gbDeviceProtocol.encodeSendWeather(weatherSpec); sendToDevice(bytes); } + + @Override + public void onSetFmFrequency(float frequency) { + byte[] bytes = gbDeviceProtocol.encodeFmFrequency(frequency); + sendToDevice(bytes); + } + + @Override + public void onSetLedColor(int color) { + byte[] bytes = gbDeviceProtocol.encodeLedColor(color); + sendToDevice(bytes); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java index 5070ce8e6..f29b1c23b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java @@ -133,4 +133,12 @@ public abstract class GBDeviceProtocol { public byte[] encodeSendWeather(WeatherSpec weatherSpec) { return null; } + + public byte[] encodeLedColor(int color) { + return null; + } + + public byte[] encodeFmFrequency(float frequency) { + return null; + } } diff --git a/app/src/main/res/drawable/ic_led_color.xml b/app/src/main/res/drawable/ic_led_color.xml new file mode 100644 index 000000000..8c0a801e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_led_color.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_radio.xml b/app/src/main/res/drawable/ic_radio.xml new file mode 100644 index 000000000..d14122596 --- /dev/null +++ b/app/src/main/res/drawable/ic_radio.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/device_itemv2.xml b/app/src/main/res/layout/device_itemv2.xml index f8ff97f1b..c780f23f5 100644 --- a/app/src/main/res/layout/device_itemv2.xml +++ b/app/src/main/res/layout/device_itemv2.xml @@ -139,13 +139,58 @@ + + + + + + + + + + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index dbcbe3cf7..2896cce0c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -9,6 +9,8 @@ Monitor de sono (ALPHA) Procurar dispositivo perdido Captura de ecrã + Mudar cor do LED + Mudar frequência FM Desligar Apagar dispositivo Apagar %1$s @@ -523,4 +525,14 @@ Calibrar Emparelhamento do Watch 9 Calibração do Watch 9 + + + + Cor do LED + + + Frequência FM + Frequência inválida + Por favor introduza uma frequência entre 87.5 e 108.0 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5092d9a06..7fd6abbcf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,6 +10,8 @@ Synchronize Find lost device Take Screenshot + Change LED Color + Change FM Frequency Connect Disconnect Delete Device @@ -634,4 +636,12 @@ Share log Please keep in mind Gadgetbridge log files may contain lots of personal information, including but not limited to health data, unique identifiers (such as a device MAC address), music preferences, etc. Consider editing the file and removing this information before sending the file to a public issue report. Warning! + + + LED Color + + + FM Frequency + Invalid frequency + Please enter a frequency between 87.5 and 108.0 diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java index e4ccc90ea..1c4bcb8da 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java @@ -188,4 +188,14 @@ class TestDeviceSupport extends AbstractDeviceSupport { public void onSendWeather(WeatherSpec weatherSpec) { } + + @Override + public void onSetFmFrequency(float frequency) { + + } + + @Override + public void onSetLedColor(int color) { + + } } From 2fe4b84a1060e50ff27cef4fada458516616dba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Fri, 31 Aug 2018 12:39:51 +0100 Subject: [PATCH 2/2] Roidmi: Initial Support Roidmi 3 support is disabled for now, since it is not working. --- .../devices/roidmi/Roidmi1Coordinator.java | 57 ++++++ .../devices/roidmi/Roidmi3Coordinator.java | 58 ++++++ .../devices/roidmi/RoidmiConst.java | 36 ++++ .../devices/roidmi/RoidmiCoordinator.java | 135 ++++++++++++++ .../gadgetbridge/model/DeviceType.java | 2 + .../service/DeviceSupportFactory.java | 7 + .../service/btclassic/BtClassicIoThread.java | 4 + .../devices/roidmi/Roidmi1Protocol.java | 150 +++++++++++++++ .../devices/roidmi/Roidmi3Protocol.java | 154 ++++++++++++++++ .../devices/roidmi/RoidmiIoThread.java | 70 +++++++ .../devices/roidmi/RoidmiProtocol.java | 93 ++++++++++ .../service/devices/roidmi/RoidmiSupport.java | 171 ++++++++++++++++++ .../gadgetbridge/util/DeviceHelper.java | 4 + .../res/drawable-hdpi/ic_device_roidmi.png | Bin 0 -> 3713 bytes .../ic_device_roidmi_disabled.png | Bin 0 -> 3301 bytes .../res/drawable-mdpi/ic_device_roidmi.png | Bin 0 -> 2981 bytes .../ic_device_roidmi_disabled.png | Bin 0 -> 2813 bytes .../res/drawable-xhdpi/ic_device_roidmi.png | Bin 0 -> 4558 bytes .../ic_device_roidmi_disabled.png | Bin 0 -> 3945 bytes .../res/drawable-xxhdpi/ic_device_roidmi.png | Bin 0 -> 4204 bytes .../ic_device_roidmi_disabled.png | Bin 0 -> 3891 bytes app/src/main/res/values/strings.xml | 2 + 22 files changed, 943 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java create mode 100644 app/src/main/res/drawable-hdpi/ic_device_roidmi.png create mode 100644 app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png create mode 100644 app/src/main/res/drawable-mdpi/ic_device_roidmi.png create mode 100644 app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_device_roidmi.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java new file mode 100644 index 000000000..47351dd01 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.bluetooth.BluetoothDevice; +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class Roidmi1Coordinator extends RoidmiCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi1Coordinator.class); + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + try { + BluetoothDevice device = candidate.getDevice(); + String name = device.getName(); + + if (name != null && name.contains("睿米车载蓝牙播放器")) { + return DeviceType.ROIDMI; + } + } catch (Exception ex) { + LOG.error("unable to check device support", ex); + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.ROIDMI; + } + + @Override + public int[] getColorPresets() { + return RoidmiConst.COLOR_PRESETS; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java new file mode 100644 index 000000000..10c351844 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java @@ -0,0 +1,58 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.bluetooth.BluetoothDevice; +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class Roidmi3Coordinator extends RoidmiCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi3Coordinator.class); + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + try { + BluetoothDevice device = candidate.getDevice(); + String name = device.getName(); + + if (name != null && name.contains("Roidmi Music Blue C")) { + LOG.warn("Found a Roidmi 3, but support is disabled."); + return DeviceType.UNKNOWN; // TODO Roidmi 3 is not working atm + } + } catch (Exception ex) { + LOG.error("unable to check device support", ex); + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.ROIDMI3; + } + + @Override + public boolean supportsRgbLedColor() { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java new file mode 100644 index 000000000..f7992aa76 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java @@ -0,0 +1,36 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.graphics.Color; + +public class RoidmiConst { + public static final String ACTION_GET_LED_COLOR = "roidmi_get_led_color"; + public static final String ACTION_GET_FM_FREQUENCY = "roidmi_get_frequency"; + public static final String ACTION_GET_VOLTAGE = "roidmi_get_voltage"; + + public static final int[] COLOR_PRESETS = new int[]{ + Color.rgb(0xFF, 0x00, 0x00), // red + Color.rgb(0x00, 0xFF, 0x00), // green + Color.rgb(0x00, 0x00, 0xFF), // blue + Color.rgb(0xFF, 0xFF, 0x01), // yellow + Color.rgb(0x00, 0xAA, 0xE5), // sky blue + Color.rgb(0xF0, 0x6E, 0xAA), // pink + Color.rgb(0xFF, 0xFF, 0xFF), // white + Color.rgb(0x00, 0x00, 0x00), // black + }; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java new file mode 100644 index 000000000..39d7ce76e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java @@ -0,0 +1,135 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.app.Activity; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +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.model.ActivitySample; + +public abstract class RoidmiCoordinator extends AbstractDeviceCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiCoordinator.class); + + @Override + public String getManufacturer() { + return "Roidmi"; + } + + @Override + public int getBondingStyle(GBDevice device) { + return BONDING_STYLE_BOND; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return false; + } + + @Override + public boolean supportsActivityTracking() { + return false; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public boolean supportsAlarmConfiguration() { + return false; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return false; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return false; + } + + @Override + public boolean supportsWeather() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return false; + } + + @Override + public boolean supportsLedColor() { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 399d67f72..388a98aa2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -49,6 +49,8 @@ public enum DeviceType { ZETIME(80, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_mykronoz_zetime), ID115(90, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_id115), WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9), + ROIDMI(100, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi), + ROIDMI3(102, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi3), TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test); private final int key; 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 dd86a6c8d..10962047a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.Ama import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support; @@ -164,6 +165,12 @@ public class DeviceSupportFactory { case WATCH9: deviceSupport = new ServiceDeviceSupport(new Watch9DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case ROIDMI: + deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; + case ROIDMI3: + deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), 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/btclassic/BtClassicIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java index 5724bdfba..efa41e746 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java @@ -81,6 +81,10 @@ public abstract class BtClassicIoThread extends GBDeviceIoThread { public synchronized void write(byte[] bytes) { if (null == bytes) return; + if (mOutStream == null) { + LOG.error("mOutStream is null"); + return; + } LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length)); try { mOutStream.write(bytes); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java new file mode 100644 index 000000000..196559e35 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java @@ -0,0 +1,150 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.RoidmiConst; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class Roidmi1Protocol extends RoidmiProtocol { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi1Protocol.class); + + public Roidmi1Protocol(GBDevice device) { + super(device); + } + + private static final byte[] PACKET_HEADER = new byte[]{(byte) 0xaa, 0x55}; + private static final byte[] PACKET_TRAILER = new byte[]{(byte) 0xc3, 0x3c}; + private static final byte COMMAND_SET_FREQUENCY = 0x10; + private static final byte COMMAND_GET_FREQUENCY = (byte) 0x80; + private static final byte COMMAND_SET_COLOR = 0x11; + private static final byte COMMAND_GET_COLOR = (byte) 0x81; + + private static final int PACKET_MIN_LENGTH = 6; + + private static final int LED_COLOR_RED = 1; + private static final int LED_COLOR_GREEN = 2; + private static final int LED_COLOR_BLUE = 3; + private static final int LED_COLOR_YELLOW = 4; // not official + private static final int LED_COLOR_SKY_BLUE = 5; + private static final int LED_COLOR_PINK = 6; // not official + private static final int LED_COLOR_WHITE = 7; // not official + private static final int LED_COLOR_OFF = 8; + + // Other commands: + // App periodically sends aa5502018588c33c and receives aa5506018515111804cec33c + private static final byte[] COMMAND_PERIODIC = new byte[]{(byte) 0xaa, 0x55, 0x02, 0x01, (byte) 0x85, (byte) 0x88, (byte) 0xc3, 0x3c}; + + @Override + public GBDeviceEvent[] decodeResponse(byte[] responseData) { + if (responseData.length <= PACKET_MIN_LENGTH) { + LOG.info("Response too small"); + return null; + } + + for (int i = 0; i < packetHeader().length; i++) { + if (responseData[i] != packetHeader()[i]) { + LOG.info("Invalid response header"); + return null; + } + } + + for (int i = 0; i < packetTrailer().length; i++) { + if (responseData[responseData.length - packetTrailer().length + i] != packetTrailer()[i]) { + LOG.info("Invalid response trailer"); + return null; + } + } + + if (calcChecksum(responseData) != responseData[responseData.length - packetTrailer().length - 1]) { + LOG.info("Invalid response checksum"); + return null; + } + + switch (responseData[3]) { + case COMMAND_GET_COLOR: + int color = responseData[5]; + LOG.debug("Got color: " + color); + GBDeviceEventLEDColor evColor = new GBDeviceEventLEDColor(); + evColor.color = RoidmiConst.COLOR_PRESETS[color - 1]; + return new GBDeviceEvent[]{evColor}; + case COMMAND_GET_FREQUENCY: + String frequencyHex = GB.hexdump(responseData, 4, 2); + float frequency = Float.valueOf(frequencyHex) / 10.0f; + LOG.debug("Got frequency: " + frequency); + GBDeviceEventFmFrequency evFrequency = new GBDeviceEventFmFrequency(); + evFrequency.frequency = frequency; + return new GBDeviceEvent[]{evFrequency}; + default: + LOG.error("Unrecognized response type 0x" + GB.hexdump(responseData, packetHeader().length, 1)); + return null; + } + } + + @Override + public byte[] encodeLedColor(int color) { + int[] presets = RoidmiConst.COLOR_PRESETS; + int color_id = -1; + for (int i = 0; i < presets.length; i++) { + if (presets[i] == color) { + color_id = (i + 1) & 255; + break; + } + } + + if (color_id < 0 || color_id > 8) + throw new IllegalArgumentException("color must belong to RoidmiConst.COLOR_PRESETS"); + + return encodeCommand(COMMAND_SET_COLOR, (byte) 0, (byte) color_id); + } + + @Override + public byte[] encodeFmFrequency(float frequency) { + if (frequency < 87.5 || frequency > 108.0) + throw new IllegalArgumentException("Frequency must be >= 87.5 and <= 180.0"); + + byte[] freq = frequencyToBytes(frequency); + + return encodeCommand(COMMAND_SET_FREQUENCY, freq[0], freq[1]); + } + + public byte[] encodeGetLedColor() { + return encodeCommand(COMMAND_GET_COLOR, (byte) 0, (byte) 0); + } + + public byte[] encodeGetFmFrequency() { + return encodeCommand(COMMAND_GET_FREQUENCY, (byte) 0, (byte) 0); + } + + public byte[] encodeGetVoltage() { + return null; + } + + public byte[] packetHeader() { + return PACKET_HEADER; + } + + public byte[] packetTrailer() { + return PACKET_TRAILER; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java new file mode 100644 index 000000000..a4d6412e7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java @@ -0,0 +1,154 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class Roidmi3Protocol extends RoidmiProtocol { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi3Protocol.class); + + public Roidmi3Protocol(GBDevice device) { + super(device); + } + + // Commands below need to be wrapped in a packet + + private static final byte[] COMMAND_GET_COLOR = new byte[]{0x02, (byte) 0x81}; + private static final byte[] COMMAND_GET_FREQUENCY = new byte[]{0x05, (byte) 0x81}; + private static final byte[] COMMAND_GET_VOLTAGE = new byte[]{0x06, (byte) 0x81}; + + private static final byte[] COMMAND_SET_COLOR = new byte[]{0x02, 0x01, 0x00, 0x00, 0x00}; + private static final byte[] COMMAND_SET_FREQUENCY = new byte[]{0x05, (byte) 0x81, 0x09, 0x64}; + private static final byte[] COMMAND_DENOISE_ON = new byte[]{0x05, 0x06, 0x12}; + private static final byte[] COMMAND_DENOISE_OFF = new byte[]{0x05, 0x06, 0x00}; + + private static final byte RESPONSE_COLOR = 0x02; + private static final byte RESPONSE_FREQUENCY = 0x05; + private static final byte RESPONSE_VOLTAGE = 0x06; + // Next response byte is always 0x81, followed by the value + + private static final int PACKET_MIN_LENGTH = 4; + + @Override + public GBDeviceEvent[] decodeResponse(byte[] res) { + if (res.length <= PACKET_MIN_LENGTH) { + LOG.info("Response too small"); + return null; + } + + if (calcChecksum(res) != res[res.length - 2]) { + LOG.info("Invalid response checksum"); + return null; + } + + if (res[0] + 2 != res.length) { + LOG.info("Packet length doesn't match"); + return null; + } + + if (res[1] != (byte) 0x81) { + LOG.error("Unrecognized response" + GB.hexdump(res, 0, res.length)); + return null; + } + + if (res[1] == RESPONSE_VOLTAGE) { + String voltageHex = GB.hexdump(res, 3, 2); + float voltage = Float.valueOf(voltageHex) / 10.0f; + LOG.debug("Got voltage: " + voltage); + GBDeviceEventBatteryInfo evBattery = new GBDeviceEventBatteryInfo(); + evBattery.voltage = voltage; + return new GBDeviceEvent[]{evBattery}; + } else if (res[1] == RESPONSE_COLOR) { + LOG.debug("Got color: " + GB.hexdump(res, 3, 3)); + int color = res[3] << 16 | res[4] << 8 | res[4]; + GBDeviceEventLEDColor evColor = new GBDeviceEventLEDColor(); + evColor.color = color; + return new GBDeviceEvent[]{evColor}; + } else if (res[1] == RESPONSE_FREQUENCY) { + String frequencyHex = GB.hexdump(res, 3, 2); + float frequency = Float.valueOf(frequencyHex) / 10.0f; + LOG.debug("Got frequency: " + frequency); + GBDeviceEventFmFrequency evFrequency = new GBDeviceEventFmFrequency(); + evFrequency.frequency = frequency; + return new GBDeviceEvent[]{evFrequency}; + } else { + LOG.error("Unrecognized response" + GB.hexdump(res, 0, res.length)); + return null; + } + } + + @Override + public byte[] encodeLedColor(int color) { + byte[] cmd = COMMAND_SET_COLOR.clone(); + + cmd[2] = (byte) color; + cmd[3] = (byte) (color >> 8); + cmd[4] = (byte) (color >> 16); + + return encodeCommand(cmd); + } + + @Override + public byte[] encodeFmFrequency(float frequency) { + if (frequency < 87.5 || frequency > 108.0) + throw new IllegalArgumentException("Frequency must be >= 87.5 and <= 180.0"); + + byte[] cmd = COMMAND_SET_FREQUENCY.clone(); + byte[] freq = frequencyToBytes(frequency); + cmd[2] = freq[0]; + cmd[3] = freq[1]; + + return encodeCommand(cmd); + } + + @Override + public byte[] encodeGetLedColor() { + return encodeCommand(COMMAND_GET_COLOR); + } + + @Override + public byte[] encodeGetFmFrequency() { + return encodeCommand(COMMAND_GET_FREQUENCY); + } + + @Override + public byte[] packetHeader() { + return new byte[0]; + } + + @Override + public byte[] packetTrailer() { + return new byte[0]; + } + + public byte[] encodeGetVoltage() { + return COMMAND_GET_VOLTAGE; + } + + public byte[] encodeDenoise(boolean enabled) { + byte[] cmd = enabled ? COMMAND_DENOISE_ON : COMMAND_DENOISE_OFF; + return encodeCommand(cmd); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java new file mode 100644 index 000000000..012f0c28d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java @@ -0,0 +1,70 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class RoidmiIoThread extends BtClassicIoThread { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiIoThread.class); + + private final byte[] HEADER; + private final byte[] TRAILER; + + public RoidmiIoThread(GBDevice gbDevice, Context context, RoidmiProtocol roidmiProtocol, RoidmiSupport roidmiSupport, BluetoothAdapter roidmiBtAdapter) { + super(gbDevice, context, roidmiProtocol, roidmiSupport, roidmiBtAdapter); + + HEADER = roidmiProtocol.packetHeader(); + TRAILER = roidmiProtocol.packetTrailer(); + } + + @Override + protected byte[] parseIncoming(InputStream inputStream) throws IOException { + ByteArrayOutputStream msgStream = new ByteArrayOutputStream(); + + boolean finished = false; + byte[] incoming = new byte[1]; + + while (!finished) { + inputStream.read(incoming); + msgStream.write(incoming); + + byte[] arr = msgStream.toByteArray(); + if (arr.length > HEADER.length) { + int expectedLength = HEADER.length + TRAILER.length + arr[HEADER.length] + 2; + if (arr.length == expectedLength) { + finished = true; + } + } + } + + byte[] msgArray = msgStream.toByteArray(); + LOG.debug("Packet: " + GB.hexdump(msgArray, 0, msgArray.length)); + return msgArray; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java new file mode 100644 index 000000000..07f809905 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java @@ -0,0 +1,93 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public abstract class RoidmiProtocol extends GBDeviceProtocol { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiProtocol.class); + + // Packet structure: HEADER N_PARAMS PARAM_1 ... PARAM_N CHECKSUM TRAILER + + public RoidmiProtocol(GBDevice device) { + super(device); + } + + @Override + public abstract GBDeviceEvent[] decodeResponse(byte[] responseData); + + @Override + public abstract byte[] encodeLedColor(int color); + + @Override + public abstract byte[] encodeFmFrequency(float frequency); + + public abstract byte[] encodeGetLedColor(); + + public abstract byte[] encodeGetFmFrequency(); + + public abstract byte[] encodeGetVoltage(); + + public abstract byte[] packetHeader(); + + public abstract byte[] packetTrailer(); + + public byte[] encodeCommand(byte... params) { + byte[] cmd = new byte[packetHeader().length + packetTrailer().length + params.length + 2]; + + for (int i = 0; i < packetHeader().length; i++) + cmd[i] = packetHeader()[i]; + for (int i = 0; i < packetTrailer().length; i++) + cmd[cmd.length - packetTrailer().length + i] = packetTrailer()[i]; + + cmd[packetHeader().length] = (byte) params.length; + for (int i = 0; i < params.length; i++) { + cmd[packetHeader().length + 1 + i] = params[i]; + } + cmd[cmd.length - packetTrailer().length - 1] = calcChecksum(cmd); + + return cmd; + } + + public byte calcChecksum(byte[] packet) { + int chk = 0; + for (int i = packetHeader().length; i < packet.length - packetTrailer().length - 1; i++) { + chk += packet[i] & 255; + } + return (byte) chk; + } + + public byte[] frequencyToBytes(float frequency) { + byte[] res = new byte[2]; + String format = String.format(Locale.getDefault(), "%04d", (int) (10.0f * frequency)); + try { + res[0] = (byte) (Integer.parseInt(format.substring(0, 2), 16) & 255); + res[1] = (byte) (Integer.parseInt(format.substring(2), 16) & 255); + } catch (Exception e) { + LOG.error(e.getMessage()); + } + + return res; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java new file mode 100644 index 000000000..7736387a0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java @@ -0,0 +1,171 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import android.net.Uri; +import android.os.Handler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.RoidmiConst; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class RoidmiSupport extends AbstractSerialDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiSupport.class); + + private final Handler handler = new Handler(); + private int infoRequestTries = 0; + private final Runnable infosRunnable = new Runnable() { + public void run() { + infoRequestTries += 1; + + try { + boolean infoMissing = false; + + if (getDevice().getExtraInfo("led_color") == null) { + infoMissing = true; + onSendConfiguration(RoidmiConst.ACTION_GET_LED_COLOR); + } + + if (getDevice().getExtraInfo("fm_frequency") == null) { + infoMissing = true; + + onSendConfiguration(RoidmiConst.ACTION_GET_FM_FREQUENCY); + } + + if (getDevice().getType() == DeviceType.ROIDMI3) { + if (getDevice().getBatteryVoltage() == -1) { + infoMissing = true; + + onSendConfiguration(RoidmiConst.ACTION_GET_VOLTAGE); + } + } + + if (infoMissing) { + if (infoRequestTries < 6) { + requestDeviceInfos(500 + infoRequestTries * 120); + } else { + LOG.error("Failed to get Roidmi infos after 6 tries"); + } + } + } catch (Exception e) { + LOG.error("Failed to get Roidmi infos", e); + } + } + }; + + private void requestDeviceInfos(int delayMillis) { + handler.postDelayed(infosRunnable, delayMillis); + } + + @Override + public boolean connect() { + getDeviceIOThread().start(); + + requestDeviceInfos(1500); + + return true; + } + + @Override + protected GBDeviceProtocol createDeviceProtocol() { + if (getDevice().getType() == DeviceType.ROIDMI) { + return new Roidmi1Protocol(getDevice()); + } else if (getDevice().getType() == DeviceType.ROIDMI3) { + return new Roidmi3Protocol(getDevice()); + } + + LOG.error("Unsupported device type with key = " + getDevice().getType().getKey()); + return null; + } + + @Override + public void onSendConfiguration(final String config) { + LOG.debug("onSendConfiguration " + config); + + RoidmiIoThread roidmiIoThread = getDeviceIOThread(); + RoidmiProtocol roidmiProtocol = (RoidmiProtocol) getDeviceProtocol(); + + switch (config) { + case RoidmiConst.ACTION_GET_LED_COLOR: + roidmiIoThread.write(roidmiProtocol.encodeGetLedColor()); + break; + case RoidmiConst.ACTION_GET_FM_FREQUENCY: + roidmiIoThread.write(roidmiProtocol.encodeGetFmFrequency()); + break; + case RoidmiConst.ACTION_GET_VOLTAGE: + roidmiIoThread.write(roidmiProtocol.encodeGetVoltage()); + break; + default: + LOG.error("Invalid Roidmi configuration " + config); + break; + } + } + + @Override + protected GBDeviceIoThread createDeviceIOThread() { + return new RoidmiIoThread(getDevice(), getContext(), (RoidmiProtocol) getDeviceProtocol(), RoidmiSupport.this, getBluetoothAdapter()); + } + + @Override + public synchronized RoidmiIoThread getDeviceIOThread() { + return (RoidmiIoThread) super.getDeviceIOThread(); + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + public void onInstallApp(Uri uri) { + // Nothing to do + } + + @Override + public void onAppConfiguration(UUID uuid, String config, Integer id) { + // Nothing to do + } + + @Override + public void onHeartRateTest() { + // Nothing to do + } + + @Override + public void onSetConstantVibration(int intensity) { + // Nothing to do + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + // Nothing to do + } + + @Override + public void onSetAlarms(ArrayList alarms) { + // Nothing to do + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index 78255160e..cc1fc17cc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -52,6 +52,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2HRXCoor import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; @@ -217,6 +219,8 @@ public class DeviceHelper { result.add(new ZeTimeCoordinator()); result.add(new ID115Coordinator()); result.add(new Watch9DeviceCoordinator()); + result.add(new Roidmi1Coordinator()); + result.add(new Roidmi3Coordinator()); return result; } diff --git a/app/src/main/res/drawable-hdpi/ic_device_roidmi.png b/app/src/main/res/drawable-hdpi/ic_device_roidmi.png new file mode 100644 index 0000000000000000000000000000000000000000..84b60d63150514403c86f5e3eb42096411025e81 GIT binary patch literal 3713 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_device_roidmi.png b/app/src/main/res/drawable-mdpi/ic_device_roidmi.png new file mode 100644 index 0000000000000000000000000000000000000000..8e3af1efb52d40d9f203add783cbe1d0df7c1466 GIT binary patch literal 2981 zcmb_e3se(V8V(2uD6MOyC|XDZE22y$GnqUkgm;4yR0skhTPHI^h$NYi8D6RQz%HUz zEl2SMiVsvo#nws{w7#iFeBc{VgnDcXx{AUce1OPKfKa=Ky4^jSb0%|V=DXkb{r~;{ zxwCv`)MqZv?#?V0%Oye)8qJ(}mc!A3`H!=Gy^J|I85Ic>ixu^Y<*<2hOK_dVa_H5n z;^}y0q!c6dTofnO1UE}>WY8>@zdXx`VyOhpRudYnLB@G@>?DV+#bum0fs&^*h7c)Q zh1o>Jn4?sfITe%O9C-lSKTFCG=m{ESXX$kYN}46(^zllWvE??%VfR7kR2e78BFK(c z&SZy>CW0;C3IPm4V75rYg@il_jQFu(9wY*JA`s#OJV?qDO8FA@z>mYIneb$3bZGd1 z7IP)zq|mfc3WAxLncPf1mo#ZWNFtGdJQ##wfI$FMwt+^o00ZS^We6oG%%nBaTGGI_ zFrsQQotAMJr~M)5jf1oXY9LKa!oV!l2tr(*C8Rzejt%0B=_XyDIF5mYj?fbZnqsif zAl8^d(j=8a{sHyieN( za*Q2*pSDsdjWAF&YQTtyP#K2_k*n3>QWRAui^T{Z5TYU+K#~yxKzTd@5Q!xyPk<8$ zf=R6NLrE;%vXGYf{WXn~7{l?KDvpTIWFCwHutDkw=h0Wz!#`RkfnHkQ?{}MG4WB% zx={no5sYdEMmlYPm7Yhye2kA`0G}^X1E|%pHyMNSA z1ioL95n%GH&{`a159r1aX@d^feI+SHF-xn-IGClz2%Ix;TRX)1{HaU~%+E|A7}BAu zuV0uVlj%&@r_64)sKP>y{3db?mF-ak+XuMgNpBU7-WBgyn+*gIt4F~kG`{Q$I zbkX9 ztWy=%Hbd>R)4%u1o`X;Q-~71z?)AH+YevS~6$IEV0`qsTNKZ*S6wfW`diX>0NwK-y z{@R_f^HN&stK0(Q(Y`)kdGCYNrJd0p%bd!FC(@Dbvo-QJI(6DYZbzzon0#CL)6wo< z2)Tu%-_M7|OIGfTY@M5W_EK{~t?SB*$MSY}hF5h|=>4h=W1}}*>SQa=*-S4Qund3CBC)~4DNIG z-94;b=XSl|3Nhx%`d;=o-$chvzSV|{W{t`iIU=n(Y?S@Y?Z~V*;jF~^w+E^Zk&Vx9 zQ}<7Q9U_=l&)m3=mIjp)VR^ML1u8rSE}w}Xye*b5&9JWFaOuoSYQ?;T_8KkXcMAeeK2 zwPuT!$FC1CY}_C+vj=i{>YUDmR&ox{aA)FtUAd#X^{!>DLT@0@2jL)HDf*PJiIP$q1dC= zPHw1P&;x$FD%hn+d*}JL*>k4adItrh*+}5@9KW(VSC{Y2tBE~n(nveor)iunUv5ii zD58|(($VVmb>}uFIXk#dEcaQ6w!c_;v(`=4 zSUcCkw>$E4bIbGd>|;*!^gO}}Q!Apnraa%(wB_RAq>jppgwHkgTSZG3v_Ec3K9|py zc+IzaT69RY%3N?~@4@QESB;%h+{V{t|K$92MR#j>_bJjZ?t}BMcl7R26o$_Slv8GR zExGERkhNh{HR80)Wkli9Npc&vsryyuU0GRSXRaODh2*8}S4eFi?*i)^)u(T}oY-_@ we(}p&MQ&qymj~0$lhcp-cuaWQt?RY@MAKMa{OZLB%ZGGCSX5|5@VwlA0qb>Zw*UYD literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..f6e6526f519090d8f00e0c69a4afc350ddfc9061 GIT binary patch literal 2813 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..be31b80257d8a71800b8d12102e149dd0e624eaf GIT binary patch literal 3945 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 D!u1&x literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png new file mode 100644 index 0000000000000000000000000000000000000000..34fec7f48af679f9dc5f2eabc06edcc10fb3b036 GIT binary patch literal 4204 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..1c67417d0a4f64d94bf32819de79e9864129f4cd GIT binary patch literal 3891 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7fd6abbcf..32df5a596 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -607,6 +607,8 @@ MyKronoz ZeTime ID115 Watch 9 + Roidmi + Roidmi 3 Choose export location Gadgetbridge notifications