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 extends Activity> getPairingActivity() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsActivityTracking() {
+ return false;
+ }
+
+ @Override
+ public SampleProvider extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
+ return null;
+ }
+
+ @Override
+ public 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 extends Activity> 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 extends Alarm> 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 000000000..84b60d631
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_roidmi.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png
new file mode 100644
index 000000000..a7c66947b
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png differ
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 000000000..8e3af1efb
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_roidmi.png differ
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 000000000..f6e6526f5
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_device_roidmi.png b/app/src/main/res/drawable-xhdpi/ic_device_roidmi.png
new file mode 100644
index 000000000..fdd34e5bc
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_roidmi.png differ
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 000000000..be31b8025
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png differ
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 000000000..34fec7f48
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png differ
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 000000000..1c67417d0
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png differ
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