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 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