From fb3a858263037dea08c1e83a63e2899be11071a1 Mon Sep 17 00:00:00 2001 From: Daniel Dakhno Date: Mon, 27 Dec 2021 14:54:58 +0100 Subject: [PATCH 01/16] fixed testCases --- .../gadgetbridge/service/TestDeviceSupport.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 360c26027..aa71841db 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java @@ -15,6 +15,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; class TestDeviceSupport extends AbstractDeviceSupport { @@ -64,6 +65,11 @@ class TestDeviceSupport extends AbstractDeviceSupport { } + @Override + public void onSetReminders(ArrayList reminders) { + + } + @Override public void onSetCallState(CallSpec callSpec) { @@ -203,4 +209,9 @@ class TestDeviceSupport extends AbstractDeviceSupport { public void onSetLedColor(int color) { } + + @Override + public void onPowerOff() { + + } } From b0ed617072a7cae3b3ea433aaa1e28c93fc523d6 Mon Sep 17 00:00:00 2001 From: dakhnod Date: Mon, 27 Dec 2021 15:37:04 +0100 Subject: [PATCH 02/16] device-bose-qc35 (#2520) This PR adds not only the device Bose QC35, it also adds the following autop-reconnect feature: When the headphones are turned on, the initiate a connection with the phone. With this change, GB is notified about said change, and tries to establish a connection to the newly connected device, if the appropriate device setting is set. The QC35 headpones always have NC turned on after boot, thus the main feature of this implementation is to turn off NC as soon as the headphones are turned on and connected to the phone. I am open for discussion regarding the implementation, but this seems like a good first proposal. What is missing is the ability to connect to multiple devices, since in many cases headphones can be connected to the watch simultaniously to a smartwatch or other gadget. Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2520 Co-authored-by: dakhnod Co-committed-by: dakhnod --- .../DeviceSettingsPreferenceConst.java | 2 + .../DeviceSpecificSettingsFragment.java | 3 + .../devices/qc35/QC35Coordinator.java | 145 ++++++++++++++++++ .../gadgetbridge/model/DeviceType.java | 1 + .../service/DeviceSupportFactory.java | 4 + .../service/devices/qc35/QC35BaseSupport.java | 89 +++++++++++ .../service/devices/qc35/QC35IOThread.java | 78 ++++++++++ .../service/devices/qc35/QC35Protocol.java | 90 +++++++++++ .../gadgetbridge/util/DeviceHelper.java | 2 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/devicesettings_qc35.xml | 9 ++ 11 files changed, 425 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qc35/QC35Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35BaseSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35IOThread.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35Protocol.java create mode 100644 app/src/main/res/xml/devicesettings_qc35.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index 231912ae6..a7318161c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -133,6 +133,8 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_SONY_AUTOMATIC_POWER_OFF = "pref_sony_automatic_power_off"; public static final String PREF_SONY_NOTIFICATION_VOICE_GUIDE = "pref_sony_notification_voice_guide"; + public static final String PREF_QC35_NOISE_CANCELLING_LEVEL = "qc35_noise_cancelling_level"; + public static final String PREFS_ACTIVITY_IN_DEVICE_CARD = "prefs_activity_in_device_card"; public static final String PREFS_ACTIVITY_IN_DEVICE_CARD_STEPS = "prefs_activity_in_device_card_steps"; public static final String PREFS_ACTIVITY_IN_DEVICE_CARD_SLEEP = "prefs_activity_in_device_card_sleep"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index e0371502e..92f04e4cd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -152,6 +152,7 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD_SLEEP; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD_STEPS; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREFS_DEVICE_CHARTS_TABS; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_QC35_NOISE_CANCELLING_LEVEL; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_ACTIVATE_DISPLAY_ON_LIFT; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST; @@ -569,6 +570,8 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp addPreferenceHandlerFor(PREF_SONY_AUTOMATIC_POWER_OFF); addPreferenceHandlerFor(PREF_SONY_NOTIFICATION_VOICE_GUIDE); + addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL); + String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF); boolean sleepTimeScheduled = sleepTimeState.equals(PREF_DO_NOT_DISTURB_SCHEDULED); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qc35/QC35Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qc35/QC35Coordinator.java new file mode 100644 index 000000000..ff144f3e2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qc35/QC35Coordinator.java @@ -0,0 +1,145 @@ +/* Copyright (C) 2021 Daniel Dakhno + + 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.qc35; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +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.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class QC35Coordinator extends AbstractDeviceCoordinator { + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + if (candidate.getName().startsWith("Bose QC 35")) { + return DeviceType.BOSE_QC35; + } + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.BOSE_QC35; + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public int[] getSupportedDeviceSpecificSettings(GBDevice device) { + return new int[]{ + R.xml.devicesettings_qc35 + }; + } + + @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 int getAlarmSlotCount() { + return 0; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return false; + } + + @Override + public String getManufacturer() { + return "Bose"; + } + + @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; + } +} 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 ff3dcc3c8..83fc145ef 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -105,6 +105,7 @@ public enum DeviceType { GALAXY_BUDS(420, R.drawable.ic_device_galaxy_buds, R.drawable.ic_device_galaxy_buds_disabled, R.string.devicetype_galaxybuds), SONY_WH_1000XM3(430, R.drawable.ic_device_headphones, R.drawable.ic_device_headphones_disabled, R.string.devicetype_sony_wh_1000xm3), SONY_WF_SP800N(431, R.drawable.ic_device_galaxy_buds, R.drawable.ic_device_galaxy_buds_disabled, R.string.devicetype_sony_wf_sp800n), + BOSE_QC35(440, R.drawable.ic_device_headphones, R.drawable.ic_device_headphones_disabled, R.string.devicetype_bose_qc35), 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 bef134867..eda89f9b9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -85,6 +85,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.nothing.Ear1Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.nut.NutSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pinetime.PineTimeJFSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qc35.QC35BaseSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.smaq2oss.SMAQ2OSSSupport; @@ -376,6 +377,9 @@ public class DeviceSupportFactory { case SONY_WF_SP800N: deviceSupport = new ServiceDeviceSupport(new SonyHeadphonesSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case BOSE_QC35: + deviceSupport = new ServiceDeviceSupport(new QC35BaseSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35BaseSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35BaseSupport.java new file mode 100644 index 000000000..5efee2a84 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35BaseSupport.java @@ -0,0 +1,89 @@ +/* Copyright (C) 2021 Daniel Dakhno + + 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.qc35; + +import android.net.Uri; + +import java.util.ArrayList; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class QC35BaseSupport extends AbstractSerialDeviceSupport { + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config, Integer id) { + + } + + @Override + public void onHeartRateTest() { + + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + + } + + @Override + public void onReadConfiguration(String config) { + + } + + + + @Override + public boolean connect() { + getDeviceProtocol(); + getDeviceIOThread().start(); + return true; + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + protected GBDeviceProtocol createDeviceProtocol() { + return new QC35Protocol(getDevice()); + } + + @Override + protected GBDeviceIoThread createDeviceIOThread() { + return new QC35IOThread(getDevice(), getContext(), (QC35Protocol) createDeviceProtocol(), this, getBluetoothAdapter()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35IOThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35IOThread.java new file mode 100644 index 000000000..964791bd1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35IOThread.java @@ -0,0 +1,78 @@ +/* Copyright (C) 2021 Daniel Dakhno + + 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.qc35; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.os.ParcelUuid; + +import androidx.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; + +public class QC35IOThread extends BtClassicIoThread { + QC35Protocol protocol; + byte[] buffer = new byte[1024]; + + private Logger logger = LoggerFactory.getLogger(getClass()); + + public QC35IOThread(GBDevice gbDevice, Context context, QC35Protocol deviceProtocol, AbstractSerialDeviceSupport deviceSupport, BluetoothAdapter btAdapter) { + super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter); + this.protocol = deviceProtocol; + } + + @NonNull + @Override + protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) { + return UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); + } + + @Override + protected void initialize() { + super.initialize(); + + byte[] connectPayload = new byte[]{0x00, 0x01, 0x01, 0x00}; + byte[] ncPayload = protocol.encodeSendConfiguration(DeviceSettingsPreferenceConst.PREF_QC35_NOISE_CANCELLING_LEVEL); + byte[] batteryPayload = new byte[]{0x02, 0x02, 0x01, 0x00}; + byte[] packet = new byte[connectPayload.length + ncPayload.length + batteryPayload.length]; + System.arraycopy(connectPayload, 0, packet, 0, connectPayload.length); + System.arraycopy(ncPayload, 0, packet, connectPayload.length, ncPayload.length); + System.arraycopy(batteryPayload, 0, packet, ncPayload.length + connectPayload.length, batteryPayload.length); + + getDevice().setFirmwareVersion("0"); + + write(packet); + } + @Override + protected byte[] parseIncoming(InputStream inStream) throws IOException { + int size = inStream.read(buffer); + logger.debug("read bytes: {}", size); + byte[] actual = new byte[size]; + System.arraycopy(buffer, 0, actual, 0, size); + return actual; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35Protocol.java new file mode 100644 index 000000000..c7f026a46 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qc35/QC35Protocol.java @@ -0,0 +1,90 @@ +/* Copyright (C) 2021 Daniel Dakhno + + 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.qc35; + +import android.content.SharedPreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.ArrayList; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; + +public class QC35Protocol extends GBDeviceProtocol { + Logger logger = LoggerFactory.getLogger(getClass()); + protected QC35Protocol(GBDevice device) { + super(device); + } + + @Override + public GBDeviceEvent[] decodeResponse(byte[] responseData) { + logger.debug("response: {}", StringUtils.bytesToHex(responseData)); + + ArrayList events = new ArrayList<>(); + + ByteBuffer buffer = ByteBuffer.wrap(responseData); + while(buffer.remaining() > 0){ + int first = buffer.get(); + int second = buffer.get(); + int third = buffer.get(); + int length = buffer.get(); + byte[] data = new byte[length]; + buffer.get(data); + if(first == 0x02){ + if(second == 0x02){ + if(third == 0x03){ + GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + batteryInfo.level = data[0]; + events.add(batteryInfo); + } + } + } + } + + return events.toArray(new GBDeviceEvent[0]); + } + + @Override + public byte[] encodeTestNewFunction() { + return new byte[]{0x02, 0x02, 0x01, 0x00}; + } + + @Override + public byte[] encodeSendConfiguration(String config) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + + if(config.equals(DeviceSettingsPreferenceConst.PREF_QC35_NOISE_CANCELLING_LEVEL)){ + int level = prefs.getInt(config, 0); + if(level == 2){ + level = 1; + }else if(level == 1){ + level = 3; + } + return new byte[]{0x01, 0x06, 0x02, 0x01, (byte) level}; + } + + return null; + } +} 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 abd6b8952..a9adb78b4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -103,6 +103,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear1Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.qc35.QC35Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator; @@ -317,6 +318,7 @@ public class DeviceHelper { result.add(new GalaxyBudsLiveDeviceCoordinator()); result.add(new SonyWH1000XM3Coordinator()); result.add(new SonyWFSP800NCoordinator()); + result.add(new QC35Coordinator()); return result; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 25aae9a00..0f211a346 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1442,4 +1442,6 @@ Show circle on timeout: Enable on-device pairing confirmation On-device pairing confirmations can get annoying. Disabling them might lose you functionality. + Bose QC35 + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_qc35.xml b/app/src/main/res/xml/devicesettings_qc35.xml new file mode 100644 index 000000000..4d1899c10 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_qc35.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file From e332f5d23a18a6e3d19a0cf3087f99d74ff8cbae Mon Sep 17 00:00:00 2001 From: ITCactus Date: Mon, 27 Dec 2021 12:13:42 +0100 Subject: [PATCH 03/16] [PineTime][InfiniTime][2527] remove debug Toast and subscription to UUID_CHARACTERISTIC_MOTION_RAW_XYZ_VALUES --- .../service/devices/pinetime/PineTimeJFSupport.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java index c2f29a689..6205c8dc7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java @@ -470,7 +470,7 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL if (getSupportedServices().contains(PineTimeJFConstants.UUID_SERVICE_MOTION)) { builder.notify(getCharacteristic(PineTimeJFConstants.UUID_CHARACTERISTIC_MOTION_STEP_COUNT), true); - builder.notify(getCharacteristic(PineTimeJFConstants.UUID_CHARACTERISTIC_MOTION_RAW_XYZ_VALUES), true); + //builder.notify(getCharacteristic(PineTimeJFConstants.UUID_CHARACTERISTIC_MOTION_RAW_XYZ_VALUES), false); // issue #2527 } setInitialized(builder); @@ -635,7 +635,6 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL } else if (characteristicUUID.equals(PineTimeJFConstants.UUID_CHARACTERISTIC_MOTION_STEP_COUNT)) { int steps = BLETypeConversions.toUint32(characteristic.getValue()); if (LOG.isDebugEnabled()) { - GB.toast("Steps count: " + steps, Toast.LENGTH_SHORT, GB.INFO); LOG.debug("onCharacteristicChanged: MotionService:Steps=" + steps); } onReceiveStepsSample(steps); From 504b552f0cb5448e0b09d3b6f98e61a4785d5652 Mon Sep 17 00:00:00 2001 From: dakhnod Date: Mon, 27 Dec 2021 15:47:10 +0100 Subject: [PATCH 04/16] device-vesc (#2491) Adds Support for BLDC controller VESC connected to a BLE serial device like an HM10. Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2491 Co-authored-by: dakhnod Co-committed-by: dakhnod --- app/src/main/AndroidManifest.xml | 4 + .../devices/vesc/VescControlActivity.java | 247 ++++++++++++++++++ .../devices/vesc/VescCoordinator.java | 157 +++++++++++ .../gadgetbridge/model/DeviceType.java | 2 + .../service/DeviceSupportFactory.java | 5 + .../service/devices/vesc/CommandType.java | 33 +++ .../devices/vesc/VescBaseDeviceSupport.java | 198 ++++++++++++++ .../devices/vesc/VescDeviceSupport.java | 179 +++++++++++++ .../gadgetbridge/util/DeviceHelper.java | 2 + .../main/res/layout/activity_vesc_control.xml | 76 ++++++ app/src/main/res/values/strings.xml | 4 +- 11 files changed, 905 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vesc/VescControlActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vesc/VescCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/CommandType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/VescBaseDeviceSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/VescDeviceSupport.java create mode 100644 app/src/main/res/layout/activity_vesc_control.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a68761b1e..45e81503c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -124,6 +124,10 @@ android:name=".activities.CalBlacklistActivity" android:label="@string/title_activity_calblacklist" android:parentActivityName=".activities.SettingsActivity" /> + . */ +package nodomain.freeyourgadget.gadgetbridge.devices.vesc; + +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; +import nodomain.freeyourgadget.gadgetbridge.service.devices.vesc.VescDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class VescControlActivity extends AbstractGBActivity { + private static final String TAG = "VescControlActivity"; + private boolean volumeKeyPressed = false; + private boolean volumeKeysControl = false; + private int currentRPM = 0; + private int currentBreakCurrentMa = 0; + LocalBroadcastManager localBroadcastManager; + + private Logger logger = LoggerFactory.getLogger(getClass()); + + EditText rpmEditText, breakCurrentEditText; + + private final int DELAY_SAVE = 1000; + + Prefs preferences; + + final String PREFS_KEY_LAST_RPM = "VESC_LAST_RPM"; + final String PREFS_KEY_LAST_BREAK_CURRENT = "VESC_LAST_BREAK_CURRENT"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_vesc_control); + + localBroadcastManager = LocalBroadcastManager.getInstance(this); + + preferences = GBApplication.getPrefs(); + + initViews(); + + restoreValues(); + } + + private void restoreValues(){ + rpmEditText.setText(preferences.getInt(PREFS_KEY_LAST_RPM, 0)); + breakCurrentEditText.setText(preferences.getInt(PREFS_KEY_LAST_BREAK_CURRENT, 0)); + } + + @Override + protected void onPause() { + super.onPause(); + setCurrent(0); + } + + private boolean handleKeyPress(int keyCode, boolean isPressed) { + if (!volumeKeysControl) { + return false; + } + + if (keyCode != 24 && keyCode != 25) { + return false; + } + + if (volumeKeyPressed == isPressed) { + return true; + } + volumeKeyPressed = isPressed; + + logger.debug("volume " + (keyCode == 25 ? "down" : "up") + (isPressed ? " pressed" : " released")); + if (!isPressed) { + setCurrent(0); + return true; + } + if (keyCode == 24) { + setRPM(currentRPM); + } else { + setBreakCurrent(VescControlActivity.this.currentBreakCurrentMa); + } + + return true; + } + + Runnable rpmSaveRunnable = new Runnable() { + @Override + public void run() { + preferences.getPreferences().edit().putInt(PREFS_KEY_LAST_RPM, currentRPM).apply(); + } + }; + + Runnable breakCurrentSaveRunnable = new Runnable() { + @Override + public void run() { + preferences.getPreferences().edit().putInt(PREFS_KEY_LAST_BREAK_CURRENT, currentBreakCurrentMa).apply(); + } + }; + + private void initViews() { + ((CheckBox) findViewById(R.id.vesc_control_checkbox_volume_keys)) + .setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + VescControlActivity.this.volumeKeysControl = isChecked; + if (!isChecked) { + setRPM(0); + } + } + }); + + rpmEditText = ((EditText) findViewById(R.id.vesc_control_input_rpm)); + rpmEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + rpmEditText.removeCallbacks(rpmSaveRunnable); + rpmEditText.postDelayed(rpmSaveRunnable, DELAY_SAVE); + + String text = s.toString(); + if (text.isEmpty()) { + currentRPM = 0; + return; + } + VescControlActivity.this.currentRPM = Integer.parseInt(text); + } + }); + + breakCurrentEditText = ((EditText) findViewById(R.id.vesc_control_input_break_current)); + breakCurrentEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + breakCurrentEditText.removeCallbacks(breakCurrentSaveRunnable); + breakCurrentEditText.postDelayed(breakCurrentSaveRunnable, DELAY_SAVE); + + String text = s.toString(); + if (text.isEmpty()) { + currentBreakCurrentMa = 0; + return; + } + VescControlActivity.this.currentBreakCurrentMa = Integer.parseInt(text) * 1000; + } + }); + + View.OnTouchListener controlTouchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (v.getId() == R.id.vesc_control_button_fwd) { + setRPM(VescControlActivity.this.currentRPM); + } else { + setBreakCurrent(VescControlActivity.this.currentBreakCurrentMa); + } + } else if (event.getAction() == MotionEvent.ACTION_UP) { + setCurrent(0); + } else { + return false; + } + return true; + } + }; + + findViewById(R.id.vesc_control_button_fwd).setOnTouchListener(controlTouchListener); + findViewById(R.id.vesc_control_button_break).setOnTouchListener(controlTouchListener); + } + + private void setBreakCurrent(int breakCurrentMa) { + logger.debug("setting break current to {}", breakCurrentMa); + Intent intent = new Intent(VescDeviceSupport.COMMAND_SET_BREAK_CURRENT); + intent.putExtra(VescDeviceSupport.EXTRA_CURRENT, breakCurrentMa); + sendLocalBroadcast(intent); + } + + private void setCurrent(int currentMa) { + logger.debug("setting current to {}", currentMa); + Intent intent = new Intent(VescDeviceSupport.COMMAND_SET_CURRENT); + intent.putExtra(VescDeviceSupport.EXTRA_CURRENT, currentMa); + sendLocalBroadcast(intent); + } + + private void setRPM(int rpm) { + logger.debug("setting rpm to {}", rpm); + Intent intent = new Intent(VescDeviceSupport.COMMAND_SET_RPM); + intent.putExtra(VescDeviceSupport.EXTRA_RPM, rpm); + sendLocalBroadcast(intent); + } + + private void sendLocalBroadcast(Intent intent) { + localBroadcastManager.sendBroadcast(intent); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return handleKeyPress(keyCode, false); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return handleKeyPress(keyCode, true); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vesc/VescCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vesc/VescCoordinator.java new file mode 100644 index 000000000..f34dc2083 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vesc/VescCoordinator.java @@ -0,0 +1,157 @@ +/* Copyright (C) 2021 Daniel Dakhno + + 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.vesc; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +import android.os.ParcelUuid; + +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.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class VescCoordinator extends AbstractDeviceCoordinator { + public final static String UUID_SERVICE_SERIAL_HM10 = "0000ffe0-0000-1000-8000-00805f9b34fb"; + public final static String UUID_CHARACTERISTIC_SERIAL_TX_HM10 = "0000ffe1-0000-1000-8000-00805f9b34fb"; + + public final static String UUID_SERVICE_SERIAL_NRF = "0000ffe0-0000-1000-8000-00805f9b34fb"; + public final static String UUID_CHARACTERISTIC_SERIAL_TX_NRF = "0000ffe0-0000-1000-8000-00805f9b34fb"; + + + @Override + protected void deleteDevice(GBDevice gbDevice, Device device, DaoSession session) throws GBException { + + } + + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + ParcelUuid[] uuids = candidate.getServiceUuids(); + Logger logger = LoggerFactory.getLogger(getClass()); + for(ParcelUuid uuid: uuids){ + logger.debug("service: {}", uuid.toString()); + } + for(ParcelUuid uuid : uuids){ + if(uuid.getUuid().toString().equals(UUID_SERVICE_SERIAL_NRF)){ + return DeviceType.VESC_NRF; + }else if(uuid.getUuid().toString().equals(UUID_SERVICE_SERIAL_HM10)){ + return DeviceType.VESC_HM10; + } + } + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.VESC_HM10; // TODO: this limits this coordinator to NRF serial service + } + + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return false; + } + + @Override + public int getBondingStyle() { + return BONDING_STYLE_NONE; + } + + @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 int getAlarmSlotCount() { + return 0; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return false; + } + + @Override + public String getManufacturer() { + return "Benjamin Vedder"; + } + + @Override + public boolean supportsAppsManagement() { + return true; + } + + @Override + public Class getAppsManagementActivity() { + return VescControlActivity.class; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return false; + } + + @Override + public boolean supportsWeather() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return false; + } +} 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 83fc145ef..1b7f84755 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -106,6 +106,8 @@ public enum DeviceType { SONY_WH_1000XM3(430, R.drawable.ic_device_headphones, R.drawable.ic_device_headphones_disabled, R.string.devicetype_sony_wh_1000xm3), SONY_WF_SP800N(431, R.drawable.ic_device_galaxy_buds, R.drawable.ic_device_galaxy_buds_disabled, R.string.devicetype_sony_wf_sp800n), BOSE_QC35(440, R.drawable.ic_device_headphones, R.drawable.ic_device_headphones_disabled, R.string.devicetype_bose_qc35), + VESC_NRF(500, R.drawable.ic_devices_other, R.drawable.ic_devices_other, R.string.devicetype_vesc), + VESC_HM10(501, R.drawable.ic_devices_other, R.drawable.ic_devices_other, R.string.devicetype_vesc), 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 eda89f9b9..b35e55c97 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -93,6 +93,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.Sony import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.SonySWR12DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.tlw64.TLW64Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Support.UM25Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.vesc.VescDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.waspos.WaspOSDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport; @@ -377,6 +378,10 @@ public class DeviceSupportFactory { case SONY_WF_SP800N: deviceSupport = new ServiceDeviceSupport(new SonyHeadphonesSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case VESC_NRF: + case VESC_HM10: + deviceSupport = new ServiceDeviceSupport(new VescDeviceSupport(gbDevice.getType()), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; case BOSE_QC35: deviceSupport = new ServiceDeviceSupport(new QC35BaseSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/CommandType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/CommandType.java new file mode 100644 index 000000000..b1ce868b4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/CommandType.java @@ -0,0 +1,33 @@ +/* Copyright (C) 2021 Daniel Dakhno + + 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.vesc; + +public enum CommandType { + SET_CURRENT((byte) 0x06), + SET_CURRENT_BRAKE((byte) 0x07), + SET_RPM((byte) 0x08), + ; + byte commandByte; + + CommandType(byte commandByte){ + this.commandByte = commandByte; + } + + public byte getCommandByte(){ + return this.commandByte; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/VescBaseDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/VescBaseDeviceSupport.java new file mode 100644 index 000000000..07d918e9f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/VescBaseDeviceSupport.java @@ -0,0 +1,198 @@ +/* Copyright (C) 2021 Daniel Dakhno + + 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.vesc; + +import android.net.Uri; + +import java.util.ArrayList; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; + +public class VescBaseDeviceSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(VescBaseDeviceSupport.class); + + public VescBaseDeviceSupport() { + super(LOG); + } + + + @Override + public void onNotification(NotificationSpec notificationSpec) { + + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onSetTime() { + + } + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onSetCallState(CallSpec callSpec) { + + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config, Integer id) { + + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onFetchRecordedData(int dataTypes) { + + } + + @Override + public void onReset(int flags) { + + } + + @Override + public void onHeartRateTest() { + + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onReadConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + + } + + @Override + public boolean useAutoConnect() { + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/VescDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/VescDeviceSupport.java new file mode 100644 index 000000000..ca10ef14a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vesc/VescDeviceSupport.java @@ -0,0 +1,179 @@ +/* Copyright (C) 2021 Daniel Dakhno + + 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.vesc; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.vesc.VescCoordinator; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; + +public class VescDeviceSupport extends VescBaseDeviceSupport{ + BluetoothGattCharacteristic serialWriteCharacteristic; + + public static final String COMMAND_SET_RPM = "nodomain.freeyourgadget.gadgetbridge.vesc.command.SET_RPM"; + public static final String COMMAND_SET_CURRENT = "nodomain.freeyourgadget.gadgetbridge.vesc.command.SET_CURRENT"; + public static final String COMMAND_SET_BREAK_CURRENT = "nodomain.freeyourgadget.gadgetbridge.vesc.command.SET_BREAK_CURRENT"; + public static final String EXTRA_RPM = "EXTRA_RPM"; + public static final String EXTRA_CURRENT = "EXTRA_CURRENT"; + + private Logger logger = LoggerFactory.getLogger(getClass()); + + private DeviceType deviceType; + + public VescDeviceSupport(DeviceType type){ + super(); + logger.debug("VescDeviceSupport() {}", type); + + deviceType = type; + + if(type == DeviceType.VESC_NRF){ + addSupportedService(UUID.fromString(VescCoordinator.UUID_SERVICE_SERIAL_NRF)); + }else if(type == DeviceType.VESC_HM10){ + addSupportedService(UUID.fromString(VescCoordinator.UUID_SERVICE_SERIAL_HM10)); + } + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + logger.debug("initializing device"); + + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + + initBroadcast(); + + if(deviceType == DeviceType.VESC_NRF){ + this.serialWriteCharacteristic = getCharacteristic(UUID.fromString(VescCoordinator.UUID_CHARACTERISTIC_SERIAL_TX_NRF)); + }else if(deviceType == DeviceType.VESC_HM10){ + this.serialWriteCharacteristic = getCharacteristic(UUID.fromString(VescCoordinator.UUID_CHARACTERISTIC_SERIAL_TX_HM10)); + } + + return builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + } + + private void initBroadcast() { + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + + IntentFilter filter = new IntentFilter(); + filter.addAction(COMMAND_SET_RPM); + filter.addAction(COMMAND_SET_CURRENT); + filter.addAction(COMMAND_SET_BREAK_CURRENT); + + broadcastManager.registerReceiver(commandReceiver, filter); + } + + BroadcastReceiver commandReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if(intent.getAction().equals(COMMAND_SET_RPM)){ + VescDeviceSupport.this.setRPM( + intent.getIntExtra(EXTRA_RPM, 0) + ); + }else if(intent.getAction().equals(COMMAND_SET_BREAK_CURRENT)){ + VescDeviceSupport.this.setBreakCurrent( + intent.getIntExtra(EXTRA_CURRENT, 0) + ); + }else if(intent.getAction().equals(COMMAND_SET_CURRENT)){ + VescDeviceSupport.this.setCurrent( + intent.getIntExtra(EXTRA_CURRENT, 0) + ); + } + } + }; + + public void setCurrent(int currentMillisAmperes){ + buildAndQueryPacket(CommandType.SET_CURRENT, currentMillisAmperes); + } + + public void setBreakCurrent(int breakCurrentMillisAmperes){ + buildAndQueryPacket(CommandType.SET_CURRENT_BRAKE, breakCurrentMillisAmperes); + } + + public void setRPM(int rpm){ + buildAndQueryPacket(CommandType.SET_RPM, rpm); + } + + public void buildAndQueryPacket(CommandType commandType, Object ... args){ + byte[] data = buildPacket(commandType, args); + queryPacket(data); + } + + public void queryPacket(byte[] data){ + new TransactionBuilder("write serial packet") + .write(this.serialWriteCharacteristic, data) + .queue(getQueue()); + } + + public byte[] buildPacket(CommandType commandType, Object ... args){ + int dataLength = 0; + for(Object arg : args){ + if(arg instanceof Integer) dataLength += 4; + else if(arg instanceof Short) dataLength += 2; + } + ByteBuffer buffer = ByteBuffer.allocate(dataLength); + + for(Object arg : args){ + if(arg instanceof Integer) buffer.putInt((Integer) arg); + if(arg instanceof Short) buffer.putShort((Short) arg); + } + + return buildPacket(commandType, buffer.array()); + } + + public byte[] buildPacket(CommandType commandType, byte[] data){ + return buildPacket(commandType.getCommandByte(), data); + } + + private byte[] buildPacket(byte commandByte, byte[] data){ + byte[] contents = new byte[data.length + 1]; + contents[0] = commandByte; + System.arraycopy(data, 0, contents, 1, data.length); + return buildPacket(contents); + } + + private byte[] buildPacket(byte[] contents){ + int dataLength = contents.length; + ByteBuffer buffer = ByteBuffer.allocate(dataLength + (dataLength < 256 ? 5 : 6)); + if(dataLength < 256){ + buffer.put((byte)0x02); + buffer.put((byte)dataLength); + }else{ + buffer.put((byte) 0x03); + buffer.putShort((short) dataLength); + } + buffer.put(contents); + buffer.putShort((short) CheckSums.getCRC16(contents, 0)); + buffer.put((byte) 0x03); + + return buffer.array(); + } +} 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 a9adb78b4..8c0baf1a5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -111,6 +111,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.smaq2oss.SMAQ2OSSCoordinator import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.um25.Coordinator.UM25Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.vesc.VescCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.waspos.WaspOSCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinator; @@ -316,6 +317,7 @@ public class DeviceHelper { result.add(new Ear1Coordinator()); result.add(new GalaxyBudsDeviceCoordinator()); result.add(new GalaxyBudsLiveDeviceCoordinator()); + result.add(new VescCoordinator()); result.add(new SonyWH1000XM3Coordinator()); result.add(new SonyWFSP800NCoordinator()); result.add(new QC35Coordinator()); diff --git a/app/src/main/res/layout/activity_vesc_control.xml b/app/src/main/res/layout/activity_vesc_control.xml new file mode 100644 index 000000000..51e65c5e1 --- /dev/null +++ b/app/src/main/res/layout/activity_vesc_control.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + +