From 504b552f0cb5448e0b09d3b6f98e61a4785d5652 Mon Sep 17 00:00:00 2001 From: dakhnod Date: Mon, 27 Dec 2021 15:47:10 +0100 Subject: [PATCH] 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 @@ + + + + + + + + + + + + + + + + + + + + + + + +