UM25: added base device support for UM25C voltage meter

This commit is contained in:
Daniel Dakhno 2020-10-10 16:45:34 +02:00 committed by Andreas Shimokawa
parent 627bf033c3
commit 0fecdf0e18
13 changed files with 747 additions and 0 deletions

View File

@ -613,5 +613,8 @@
android:name=".devices.qhybrid.CalibrationActivity"
android:label="@string/qhybrid_title_calibration"
android:parentActivityName=".devices.qhybrid.HRConfigActivity" />
<activity
android:name=".devices.um25.Activity.DataActivity"
android:exported="true" />
</application>
</manifest>

View File

@ -0,0 +1,90 @@
package nodomain.freeyourgadget.gadgetbridge.devices.um25.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.TextView;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.lang.reflect.Field;
import java.util.HashMap;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Data.MeasurementData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Support.UM25Support;
public class DataActivity extends AbstractGBActivity {
private HashMap<Integer, TextView> valueViews = new HashMap<>(ValueDisplay.values().length);
private enum ValueDisplay{
VOLTAGE("voltage", "%.3fV", R.id.um25_text_voltage, 1000),
CURRENT("current", "%.4fA", R.id.um25_text_current, 1000),
WATTAGE("wattage", "%.4fW", R.id.um25_text_wattage, 1000),
;
private String variableName;
private String formatString;
private int textViewResource;
private float divisor;
ValueDisplay(String variableName, String formatString, int textViewResource, float divisor) {
this.variableName = variableName;
this.formatString = formatString;
this.textViewResource = textViewResource;
this.divisor = divisor;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_um25_data);
}
@Override
protected void onResume() {
super.onResume();
LocalBroadcastManager.getInstance(this)
.registerReceiver(
measurementReceiver,
new IntentFilter(UM25Support.ACTION_MEASUREMENT_TAKEN)
);
}
@Override
protected void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(this)
.unregisterReceiver(measurementReceiver);
}
private void displayMeasurementData(MeasurementData data){
for(ValueDisplay display : ValueDisplay.values()){
try {
TextView textView = valueViews.get(display.textViewResource);
if(textView == null){
valueViews.put(display.textViewResource, textView = findViewById(display.textViewResource));
}
Field field = data.getClass().getDeclaredField(display.variableName);
field.setAccessible(true);
float value = ((int) field.get(data)) / display.divisor;
String result = String.format(display.formatString, value);
textView.setText(result);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
private BroadcastReceiver measurementReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
MeasurementData data = (MeasurementData) intent.getSerializableExtra(UM25Support.EXTRA_KEY_MEASUREMENT_DATA);
displayMeasurementData(data);
}
};
}

View File

@ -0,0 +1,143 @@
package nodomain.freeyourgadget.gadgetbridge.devices.um25.Coordinator;
import android.app.Activity;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import cyanogenmod.app.CustomTile;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.um25.Activity.DataActivity;
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;
import nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Support.UM25Support;
public class UM25Coordinator extends AbstractDeviceCoordinator {
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@NonNull
@Override
public Collection<? extends ScanFilter> createBLEScanFilters() {
return Collections.singletonList(
new ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString(UM25Support.UUID_SERVICE))
.build()
);
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
if(!"UM25C".equals(candidate.getName())) return DeviceType.UNKNOWN;
for(ParcelUuid service : candidate.getServiceUuids()){
if(service.getUuid().toString().equals(UM25Support.UUID_SERVICE)) return DeviceType.UM25;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.UM25;
}
@Nullable
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return false;
}
@Override
public boolean supportsActivityTracking() {
return false;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public boolean supportsFindDevice() {
return false;
}
@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 "Ruideng";
}
@Override
public boolean supportsAppsManagement() {
return true;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return DataActivity.class;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
@Override
public boolean supportsWeather() {
return false;
}
}

View File

@ -94,6 +94,7 @@ public enum DeviceType {
SONY_SWR12(310, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_sonyswr12),
LIVEVIEW(320, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_liveview),
WASPOS(330, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_waspos),
UM25(350, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_um25),
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
private final int key;

View File

@ -83,6 +83,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSuppo
import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport;
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.vibratissimo.VibratissimoSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.waspos.WaspOSDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
@ -334,6 +335,9 @@ public class DeviceSupportFactory {
case WASPOS:
deviceSupport = new ServiceDeviceSupport(new WaspOSDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case UM25:
deviceSupport = new ServiceDeviceSupport(new UM25Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
}
if (deviceSupport != null) {
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);

View File

@ -0,0 +1,39 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Data;
import java.io.Serializable;
public class CaptureGroup implements Serializable {
private int index;
private int flownCurrent;
private int flownWattage;
public CaptureGroup(int index, int flownCurrent, int flownWattage) {
this.flownCurrent = flownCurrent;
this.flownWattage = flownWattage;
this.index = index;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public int getFlownCurrent() {
return flownCurrent;
}
public void setFlownCurrent(int flownCurrent) {
this.flownCurrent = flownCurrent;
}
public int getFlownWattage() {
return flownWattage;
}
public void setFlownWattage(int flownWattage) {
this.flownWattage = flownWattage;
}
}

View File

@ -0,0 +1,87 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Data;
import java.io.Serializable;
public class MeasurementData implements Serializable {
private int voltage; // voltage in millivolts
private int current; // current in milliampere
private int wattage; // wattage in milliwatt
private int temperatureCelcius;
private int temperatureFahreheit;
private CaptureGroup[] captureGroups;
private int voltageDataPositive;
private int voltageDataNegative;
private int chargedCurrent; // charged current in milliAmpereHours
private int chargedWattage; // charged current in milliWattHours
private int thresholdCurrent; // threshold current for charging detection
private int chargingSeconds;
private int cableResistance; // cable resistance in ohms
public MeasurementData(int voltage, int current, int wattage, int temperatureCelcius, int temperatureFahreheit, CaptureGroup[] captureGroups, int voltageDataPositive, int voltageDataNegative, int chargedCurrent, int chargedWattage, int thresholdCurrent, int chargingSeconds, int cableResistance) {
this.voltage = voltage;
this.current = current;
this.wattage = wattage;
this.temperatureCelcius = temperatureCelcius;
this.temperatureFahreheit = temperatureFahreheit;
this.captureGroups = captureGroups;
this.voltageDataPositive = voltageDataPositive;
this.voltageDataNegative = voltageDataNegative;
this.chargedCurrent = chargedCurrent;
this.chargedWattage = chargedWattage;
this.thresholdCurrent = thresholdCurrent;
this.chargingSeconds = chargingSeconds;
this.cableResistance = cableResistance;
}
public int getVoltage() {
return voltage;
}
public int getCurrent() {
return current;
}
public int getWattage() {
return wattage;
}
public int getTemperatureCelcius() {
return temperatureCelcius;
}
public int getTemperatureFahreheit() {
return temperatureFahreheit;
}
public CaptureGroup[] getCaptureGroups() {
return captureGroups;
}
public int getVoltageDataPositive() {
return voltageDataPositive;
}
public int getVoltageDataNegative() {
return voltageDataNegative;
}
public int getChargedCurrent() {
return chargedCurrent;
}
public int getChargedWattage() {
return chargedWattage;
}
public int getThresholdCurrent() {
return thresholdCurrent;
}
public int getChargingSeconds() {
return chargingSeconds;
}
public int getCableResistance() {
return cableResistance;
}
}

View File

@ -0,0 +1,179 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Support;
import android.net.Uri;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.UUID;
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 UM25BaseSupport extends AbstractBTLEDeviceSupport {
public UM25BaseSupport(Logger logger) {
super(logger);
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> 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) {
}
}

View File

@ -0,0 +1,157 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Support;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.UUID;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Data.CaptureGroup;
import nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Data.MeasurementData;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class UM25Support extends UM25BaseSupport {
public static final String UUID_SERVICE = "0000ffe0-0000-1000-8000-00805f9b34fb";
public static final String UUID_CHAR = "0000ffe1-0000-1000-8000-00805f9b34fb";
public static final String ACTION_MEASUREMENT_TAKEN = "com.nodomain.gadgetbridge.um25.MEASUREMENT_TAKEN";
public static final String EXTRA_KEY_MEASUREMENT_DATA = "EXTRA_MEASUREMENT_DATA";
public static final int LOOP_DELAY = 500;
private final byte[] COMMAND_UPDATE = new byte[]{(byte) 0xF0};
private final int PAYLOAD_LENGTH = 130;
private ByteBuffer buffer = ByteBuffer.allocate(PAYLOAD_LENGTH);
private static final Logger logger = LoggerFactory.getLogger(UM25Support.class);
public UM25Support() {
super(logger);
addSupportedService(UUID.fromString(UUID_SERVICE));
this.buffer.mark();
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
return builder
.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()))
.notify(getCharacteristic(UUID.fromString(UUID_CHAR)), true)
.add(new BtLEAction(null) {
@Override
public boolean expectsResult() {
return false;
}
@Override
public boolean run(BluetoothGatt gatt) {
logger.debug("initialized, starting timers");
startLoop();
return true;
}
})
.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
}
private void startLoop(){
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
executor.scheduleWithFixedDelay(this::sendReadCommand, 0, LOOP_DELAY, TimeUnit.MILLISECONDS);
}
private void sendReadCommand(){
logger.debug("sending read command");
buffer.reset();
new TransactionBuilder("send read command")
.write(getCharacteristic(UUID.fromString(UUID_CHAR)), COMMAND_UPDATE)
.queue(getQueue());
logger.debug("sent command");
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
if(!characteristic.getUuid().toString().equals(UUID_CHAR)) return false;
try{
buffer.put(characteristic.getValue());
if(buffer.position() == PAYLOAD_LENGTH){
handlePayload(buffer);
}
}catch (BufferOverflowException e){
logger.error("buffer overflow");
}
return true;
}
private void handlePayload(ByteBuffer payload){
String payloadString = StringUtils.bytesToHex(payload.array());
payloadString = payloadString.replaceAll("(..)", "$1 ");
logger.debug("payload: " + payloadString);
payload.order(ByteOrder.BIG_ENDIAN);
int voltage = payload.getShort(2);
int current = payload.getShort(4);
int wattage = payload.getShort(8);
int temperatureCelsius = payload.getShort(10);
int temperatureFahrenheit = payload.getShort(12);
final int STORAGE_START = 16;
CaptureGroup[] groups = new CaptureGroup[10];
for(int i = 0; i < 10; i++){
groups[i] = new CaptureGroup(
i,
payload.getInt(STORAGE_START + i * 4 + 0),
payload.getInt(STORAGE_START + i * 4 + 4)
);
}
int voltagePositive = payload.getShort(96);
int voltageNegative = payload.getShort(98);
int chargedCurrent = payload.getInt(102);
int chargedWattage = payload.getInt(106);
int thresholdCurrent = payload.get(111);
int chargingSeconds = payload.getInt(112);
int cableResistance = payload.getInt(122);
logger.debug("variable: " + chargedCurrent);
MeasurementData data = new MeasurementData(
voltage,
current,
wattage,
temperatureCelsius,
temperatureFahrenheit,
groups,
voltagePositive,
voltageNegative,
chargedCurrent,
chargedWattage,
thresholdCurrent,
chargingSeconds,
cableResistance
);
Intent measurementIntent = new Intent(ACTION_MEASUREMENT_TAKEN);
measurementIntent.putExtra(EXTRA_KEY_MEASUREMENT_DATA, data);
LocalBroadcastManager.getInstance(getContext())
.sendBroadcast(measurementIntent);
}
}

View File

@ -101,6 +101,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
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.vibratissimo.VibratissimoCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.waspos.WaspOSCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinator;
@ -294,6 +295,7 @@ public class DeviceHelper {
result.add(new LefunDeviceCoordinator());
result.add(new SonySWR12DeviceCoordinator());
result.add(new WaspOSCoordinator());
result.add(new UM25Coordinator());
return result;
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="-"
android:id="@+id/um25_text_voltage"
android:gravity="center_horizontal"
android:textSize="@dimen/um25_value_text_size"
android:textColor="@android:color/holo_green_dark"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="-"
android:id="@+id/um25_text_current"
android:gravity="center_horizontal"
android:textSize="@dimen/um25_value_text_size"
android:textColor="@android:color/holo_red_dark"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="-"
android:id="@+id/um25_text_wattage"
android:gravity="center_horizontal"
android:textSize="@dimen/um25_value_text_size"
android:textColor="@android:color/white"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -14,4 +14,5 @@ http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
<dimen name="nav_header_height">160dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="dialog_margin">20dp</dimen>
<dimen name="um25_value_text_size">60dp</dimen>
</resources>

View File

@ -824,6 +824,7 @@
<string name="devicetype_amazfit_x">Amazfit X</string>
<string name="devicetype_zepp_e">Zepp E</string>
<string name="devicetype_vibratissimo">Vibratissimo</string>
<string name="devicetype_um25">UM-25</string>
<string name="devicetype_liveview">LiveView</string>
<string name="devicetype_hplus">HPlus</string>
<string name="devicetype_makibes_f68">Makibes F68</string>