mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
device-cycling-sensor (#3705)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3705 Co-authored-by: Daniel Dakhno <dakhnod@gmail.com> Co-committed-by: Daniel Dakhno <dakhnod@gmail.com>
This commit is contained in:
parent
25b1f8f3fa
commit
3b01422a45
@ -129,6 +129,7 @@ public class GBDaoGenerator {
|
|||||||
addWorldClocks(schema, user, device);
|
addWorldClocks(schema, user, device);
|
||||||
addContacts(schema, user, device);
|
addContacts(schema, user, device);
|
||||||
addAppSpecificNotificationSettings(schema, device);
|
addAppSpecificNotificationSettings(schema, device);
|
||||||
|
addCyclingSample(schema, user, device);
|
||||||
|
|
||||||
Entity notificationFilter = addNotificationFilters(schema);
|
Entity notificationFilter = addNotificationFilters(schema);
|
||||||
|
|
||||||
@ -624,6 +625,18 @@ public class GBDaoGenerator {
|
|||||||
return activitySample;
|
return activitySample;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Entity addCyclingSample(Schema schema, Entity user, Entity device){
|
||||||
|
Entity cyclingSample = addEntity(schema, "CyclingSample");
|
||||||
|
addCommonTimeSampleProperties("AbstractTimeSample", cyclingSample, user, device);
|
||||||
|
|
||||||
|
cyclingSample.implementsSerializable();
|
||||||
|
|
||||||
|
cyclingSample.addIntProperty("RevolutionCount");
|
||||||
|
cyclingSample.addFloatProperty("Distance");
|
||||||
|
cyclingSample.addFloatProperty("Speed");
|
||||||
|
return cyclingSample;
|
||||||
|
}
|
||||||
|
|
||||||
private static Entity addVivomoveHrActivitySample(Schema schema, Entity user, Entity device) {
|
private static Entity addVivomoveHrActivitySample(Schema schema, Entity user, Entity device) {
|
||||||
final Entity activitySample = addEntity(schema, "VivomoveHrActivitySample");
|
final Entity activitySample = addEntity(schema, "VivomoveHrActivitySample");
|
||||||
activitySample.implementsSerializable();
|
activitySample.implementsSerializable();
|
||||||
|
@ -108,6 +108,9 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
if (!coordinator.supportsTemperatureMeasurement()) {
|
if (!coordinator.supportsTemperatureMeasurement()) {
|
||||||
tabList.remove("temperature");
|
tabList.remove("temperature");
|
||||||
}
|
}
|
||||||
|
if(!coordinator.supportsCyclingData()) {
|
||||||
|
tabList.remove("cycling");
|
||||||
|
}
|
||||||
return tabList;
|
return tabList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +154,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
return new Spo2ChartFragment();
|
return new Spo2ChartFragment();
|
||||||
case "temperature":
|
case "temperature":
|
||||||
return new TemperatureChartFragment();
|
return new TemperatureChartFragment();
|
||||||
|
case "cycling":
|
||||||
|
return new CyclingChartFragment();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -201,6 +206,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
return getString(R.string.pref_header_spo2);
|
return getString(R.string.pref_header_spo2);
|
||||||
case "temperature":
|
case "temperature":
|
||||||
return getString(R.string.menuitem_temperature);
|
return getString(R.string.menuitem_temperature);
|
||||||
|
case "cycling":
|
||||||
|
return getString(R.string.title_cycling);
|
||||||
}
|
}
|
||||||
return super.getPageTitle(position);
|
return super.getPageTitle(position);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,288 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.github.mikephil.charting.animation.Easing;
|
||||||
|
import com.github.mikephil.charting.charts.Chart;
|
||||||
|
import com.github.mikephil.charting.charts.LineChart;
|
||||||
|
import com.github.mikephil.charting.components.LegendEntry;
|
||||||
|
import com.github.mikephil.charting.components.LimitLine;
|
||||||
|
import com.github.mikephil.charting.components.XAxis;
|
||||||
|
import com.github.mikephil.charting.components.YAxis;
|
||||||
|
import com.github.mikephil.charting.data.Entry;
|
||||||
|
import com.github.mikephil.charting.data.LineData;
|
||||||
|
import com.github.mikephil.charting.data.LineDataSet;
|
||||||
|
import com.github.mikephil.charting.formatter.ValueFormatter;
|
||||||
|
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
|
public class CyclingChartFragment extends AbstractChartFragment<CyclingChartFragment.CyclingChartsData>{
|
||||||
|
private LineChart cyclingHistoryChart;
|
||||||
|
|
||||||
|
private int BACKGROUND_COLOR;
|
||||||
|
private int DESCRIPTION_COLOR;
|
||||||
|
private int CHART_TEXT_COLOR;
|
||||||
|
private int LEGEND_TEXT_COLOR;
|
||||||
|
private int CHART_LINE_COLOR_DISTANCE;
|
||||||
|
private int CHART_LINE_COLOR_SPEED;
|
||||||
|
private final Prefs prefs = GBApplication.getPrefs();
|
||||||
|
|
||||||
|
protected static class CyclingChartsData extends DefaultChartsData<LineData> {
|
||||||
|
public CyclingChartsData(LineData lineData) {
|
||||||
|
super(lineData, new TimeFormatter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTitle() {
|
||||||
|
return "Cycling data";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext());
|
||||||
|
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext());
|
||||||
|
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
|
||||||
|
|
||||||
|
if (prefs.getBoolean("chart_heartrate_color", false)) {
|
||||||
|
CHART_LINE_COLOR_DISTANCE = ContextCompat.getColor(getContext(), R.color.chart_activity_dark);
|
||||||
|
CHART_LINE_COLOR_SPEED = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
|
||||||
|
} else {
|
||||||
|
CHART_LINE_COLOR_DISTANCE = ContextCompat.getColor(getContext(), R.color.chart_activity_light);
|
||||||
|
CHART_LINE_COLOR_SPEED = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected CyclingChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||||
|
List<CyclingSample> samples = getSamples(db, device);
|
||||||
|
|
||||||
|
return new CyclingChartsDataBuilder(samples).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class CyclingChartsDataBuilder {
|
||||||
|
private final List<CyclingSample> samples;
|
||||||
|
|
||||||
|
private final List<Entry> lineEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
long averageSum;
|
||||||
|
long averageNumSamples;
|
||||||
|
|
||||||
|
public CyclingChartsDataBuilder(final List<CyclingSample> samples) {
|
||||||
|
this.samples = samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset() {
|
||||||
|
lineEntries.clear();
|
||||||
|
|
||||||
|
averageSum = 0;
|
||||||
|
averageNumSamples = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CyclingChartsData build() {
|
||||||
|
List<Entry> distanceEntries = new ArrayList<>();
|
||||||
|
List<Entry> speedEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
Float dayStart = 0f;
|
||||||
|
|
||||||
|
if(!samples.isEmpty()){
|
||||||
|
dayStart = samples.get(0).getDistance() / 1000f;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextIndex = 0;
|
||||||
|
for (CyclingSample sample : samples) {
|
||||||
|
// add distance in Km
|
||||||
|
distanceEntries.add(new Entry(sample.getTimestamp(), (sample.getDistance() / 1000f) - dayStart));
|
||||||
|
Float speed = sample.getSpeed();
|
||||||
|
speedEntries.add(new Entry(sample.getTimestamp(), (speed != null) ? (sample.getSpeed() * 3.6f) : 0));
|
||||||
|
|
||||||
|
if(nextIndex < samples.size()){
|
||||||
|
CyclingSample nextSample = samples.get(nextIndex);
|
||||||
|
if(nextSample.getSpeed() == null){
|
||||||
|
// sensor is off, doesn't report zero speed. So let's inject it outselves
|
||||||
|
speedEntries.add(new Entry(sample.getTimestamp() + 30_000, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
LineDataSet distanceSet = new LineDataSet(distanceEntries, "Cycling");
|
||||||
|
distanceSet.setLineWidth(2.2f);
|
||||||
|
distanceSet.setColor(CHART_LINE_COLOR_DISTANCE);
|
||||||
|
distanceSet.setDrawCircles(false);
|
||||||
|
distanceSet.setDrawCircleHole(false);
|
||||||
|
distanceSet.setDrawValues(true);
|
||||||
|
distanceSet.setValueTextSize(10f);
|
||||||
|
distanceSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||||
|
distanceSet.setHighlightEnabled(false);
|
||||||
|
distanceSet.setValueFormatter(new CyclingDistanceFormatter(CyclingChartFragment.this, dayStart));
|
||||||
|
distanceSet.setAxisDependency(cyclingHistoryChart.getAxisLeft().getAxisDependency());
|
||||||
|
LineData lineData = new LineData(distanceSet);
|
||||||
|
|
||||||
|
LineDataSet speedSet = new LineDataSet(speedEntries, "Speed");
|
||||||
|
speedSet.setLineWidth(2.2f);
|
||||||
|
speedSet.setColor(CHART_LINE_COLOR_SPEED);
|
||||||
|
speedSet.setDrawCircles(false);
|
||||||
|
speedSet.setDrawCircleHole(false);
|
||||||
|
speedSet.setDrawValues(true);
|
||||||
|
speedSet.setValueTextSize(10f);
|
||||||
|
speedSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||||
|
speedSet.setHighlightEnabled(true);
|
||||||
|
speedSet.setValueFormatter(new CyclingSpeedFormatter(CyclingChartFragment.this));
|
||||||
|
speedSet.setAxisDependency(cyclingHistoryChart.getAxisRight().getAxisDependency());
|
||||||
|
|
||||||
|
lineData.addDataSet(speedSet);
|
||||||
|
|
||||||
|
return new CyclingChartsData(lineData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderCharts() {
|
||||||
|
cyclingHistoryChart.animateX(ANIM_TIME, Easing.EaseInOutQuart);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupLegend(final Chart<?> chart) {
|
||||||
|
final List<LegendEntry> legendEntries = new ArrayList<>(2);
|
||||||
|
|
||||||
|
final LegendEntry distanceEntry = new LegendEntry();
|
||||||
|
distanceEntry.label = getString(R.string.activity_list_summary_distance);
|
||||||
|
distanceEntry.formColor = CHART_LINE_COLOR_DISTANCE;
|
||||||
|
legendEntries.add(distanceEntry);
|
||||||
|
|
||||||
|
final LegendEntry speedEntry = new LegendEntry();
|
||||||
|
speedEntry.label = getString(R.string.Speed);
|
||||||
|
speedEntry.formColor = CHART_LINE_COLOR_SPEED;
|
||||||
|
legendEntries.add(speedEntry);
|
||||||
|
|
||||||
|
chart.getLegend().setCustom(legendEntries);
|
||||||
|
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateChartsnUIThread(CyclingChartsData cyclingData) {
|
||||||
|
cyclingHistoryChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
|
||||||
|
cyclingHistoryChart.getXAxis().setValueFormatter(cyclingData.getXValueFormatter());
|
||||||
|
cyclingHistoryChart.getXAxis().setAvoidFirstLastClipping(true);
|
||||||
|
|
||||||
|
cyclingHistoryChart.setData(cyclingData.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
View rootView = inflater.inflate(R.layout.fragment_cycling, container, false);
|
||||||
|
|
||||||
|
cyclingHistoryChart = rootView.findViewById(R.id.chart_cycling_history);
|
||||||
|
|
||||||
|
cyclingHistoryChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||||
|
cyclingHistoryChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||||
|
configureBarLineChartDefaults(cyclingHistoryChart);
|
||||||
|
|
||||||
|
final XAxis x = cyclingHistoryChart.getXAxis();
|
||||||
|
x.setDrawLabels(true);
|
||||||
|
x.setDrawGridLines(true);
|
||||||
|
x.setEnabled(true);
|
||||||
|
x.setTextColor(CHART_TEXT_COLOR);
|
||||||
|
x.setDrawLimitLinesBehindData(true);
|
||||||
|
|
||||||
|
final YAxis yAxisLeft = cyclingHistoryChart.getAxisLeft();
|
||||||
|
yAxisLeft.setDrawGridLines(true);
|
||||||
|
yAxisLeft.setTextColor(CHART_LINE_COLOR_DISTANCE);
|
||||||
|
yAxisLeft.setEnabled(true);
|
||||||
|
yAxisLeft.setGridColor(CHART_LINE_COLOR_DISTANCE);
|
||||||
|
|
||||||
|
final YAxis yAxisRight = cyclingHistoryChart.getAxisRight();
|
||||||
|
yAxisRight.setDrawGridLines(true);
|
||||||
|
yAxisRight.setTextColor(CHART_LINE_COLOR_SPEED);
|
||||||
|
yAxisRight.setEnabled(true);
|
||||||
|
yAxisRight.setGridColor(CHART_LINE_COLOR_SPEED);
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
return rootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CyclingSample> getSamples(final DBHandler db, final GBDevice device) {
|
||||||
|
final int tsStart = getTSStart();
|
||||||
|
final int tsEnd = getTSEnd();
|
||||||
|
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||||
|
final TimeSampleProvider<CyclingSample> sampleProvider = coordinator.getCyclingSampleProvider(device, db.getDaoSession());
|
||||||
|
return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected static class CyclingDistanceFormatter extends ValueFormatter {
|
||||||
|
// private final DecimalFormat formatter = new DecimalFormat("0.00 km");
|
||||||
|
Float dayStartDistance;
|
||||||
|
CyclingChartFragment fragment;
|
||||||
|
|
||||||
|
public CyclingDistanceFormatter(CyclingChartFragment fragment, Float dayStartDistance) {
|
||||||
|
this.dayStartDistance = dayStartDistance;
|
||||||
|
this.fragment = fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPointLabel(Entry entry) {
|
||||||
|
return fragment.getString(R.string.chart_cycling_point_label_distance, entry.getY(), entry.getY() + dayStartDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class CyclingSpeedFormatter extends ValueFormatter {
|
||||||
|
CyclingChartFragment fragment;
|
||||||
|
|
||||||
|
public CyclingSpeedFormatter(CyclingChartFragment fragment) {
|
||||||
|
this.fragment = fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPointLabel(Entry entry) {
|
||||||
|
return fragment.getString(R.string.chart_cycling_point_label_speed, entry.getY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class TimeFormatter extends ValueFormatter {
|
||||||
|
DateFormat annotationDateFormat = SimpleDateFormat.getTimeInstance(DateFormat.SHORT);
|
||||||
|
Calendar cal = GregorianCalendar.getInstance();
|
||||||
|
|
||||||
|
public TimeFormatter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFormattedValue(float value) {
|
||||||
|
cal.clear();
|
||||||
|
cal.setTimeInMillis((long)(value));
|
||||||
|
Date date = cal.getTime();
|
||||||
|
return annotationDateFormat.format(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -460,4 +460,7 @@ public class DeviceSettingsPreferenceConst {
|
|||||||
public static final String PREF_AUTO_REPLY_INCOMING_CALL = "pref_auto_reply_phonecall";
|
public static final String PREF_AUTO_REPLY_INCOMING_CALL = "pref_auto_reply_phonecall";
|
||||||
public static final String PREF_AUTO_REPLY_INCOMING_CALL_DELAY = "pref_auto_reply_phonecall_delay";
|
public static final String PREF_AUTO_REPLY_INCOMING_CALL_DELAY = "pref_auto_reply_phonecall_delay";
|
||||||
public static final String PREF_SPEAK_NOTIFICATIONS_ALOUD = "pref_speak_notifications_aloud";
|
public static final String PREF_SPEAK_NOTIFICATIONS_ALOUD = "pref_speak_notifications_aloud";
|
||||||
|
|
||||||
|
public static final String PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL = "pref_cycling_persistence_interval";
|
||||||
|
public static final String PREF_CYCLING_SENSOR_WHEEL_DIAMETER = "pref_cycling_wheel_diameter";
|
||||||
}
|
}
|
||||||
|
@ -643,6 +643,9 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
|||||||
addPreferenceHandlerFor(PREF_HEARTRATE_AUTOMATIC_ENABLE);
|
addPreferenceHandlerFor(PREF_HEARTRATE_AUTOMATIC_ENABLE);
|
||||||
addPreferenceHandlerFor(PREF_SPO_AUTOMATIC_ENABLE);
|
addPreferenceHandlerFor(PREF_SPO_AUTOMATIC_ENABLE);
|
||||||
|
|
||||||
|
addPreferenceHandlerFor(PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL);
|
||||||
|
addPreferenceHandlerFor(PREF_CYCLING_SENSOR_WHEEL_DIAMETER);
|
||||||
|
|
||||||
addPreferenceHandlerFor("lock");
|
addPreferenceHandlerFor("lock");
|
||||||
|
|
||||||
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
||||||
|
@ -61,6 +61,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevelDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevelDao;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
|
||||||
@ -226,6 +227,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimeSampleProvider<CyclingSample> getCyclingSampleProvider(GBDevice device, DaoSession session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TimeSampleProvider<? extends HeartRateSample> getHeartRateMaxSampleProvider(GBDevice device, DaoSession session) {
|
public TimeSampleProvider<? extends HeartRateSample> getHeartRateMaxSampleProvider(GBDevice device, DaoSession session) {
|
||||||
return null;
|
return null;
|
||||||
@ -567,6 +573,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsCyclingData() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsRemSleep() {
|
public boolean supportsRemSleep() {
|
||||||
return false;
|
return false;
|
||||||
|
@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||||
@ -56,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.TimeSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
|
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
|
||||||
@ -189,6 +191,15 @@ public interface DeviceCoordinator {
|
|||||||
*/
|
*/
|
||||||
boolean supportsActivityTracking();
|
boolean supportsActivityTracking();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if cycling data is supported by the device
|
||||||
|
* (with this coordinator).
|
||||||
|
* This enables the ChartsActivity.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
boolean supportsCyclingData();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether the device supports recording dedicated activity tracks, like
|
* Indicates whether the device supports recording dedicated activity tracks, like
|
||||||
* walking, hiking, running, swimming, etc. and retrieving the recorded
|
* walking, hiking, running, swimming, etc. and retrieving the recorded
|
||||||
@ -287,6 +298,11 @@ public interface DeviceCoordinator {
|
|||||||
*/
|
*/
|
||||||
TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session);
|
TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sample provider for Cycling data, for the device being supported.
|
||||||
|
*/
|
||||||
|
TimeSampleProvider<CyclingSample> getCyclingSampleProvider(GBDevice device, DaoSession session);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the sample provider for max HR data, for the device being supported.
|
* Returns the sample provider for max HR data, for the device being supported.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.coordinator;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.db.CyclingSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSampleDao;
|
||||||
|
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.service.DeviceSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.cycling_sensor.support.CyclingSensorSupport;
|
||||||
|
|
||||||
|
public class CyclingSensorCoordinator extends AbstractBLEDeviceCoordinator {
|
||||||
|
@Override
|
||||||
|
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||||
|
final Long deviceId = device.getId();
|
||||||
|
|
||||||
|
session.getCyclingSampleDao().queryBuilder()
|
||||||
|
.where(CyclingSampleDao.Properties.DeviceId.eq(deviceId))
|
||||||
|
.buildDelete().executeDeleteWithoutDetachingEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(GBDeviceCandidate candidate) {
|
||||||
|
return candidate.supportsService(CyclingSensorSupport.UUID_CYCLING_SENSOR_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsCyclingData() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsActivityTracking() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimeSampleProvider<CyclingSample> getCyclingSampleProvider(GBDevice device, DaoSession session) {
|
||||||
|
return new CyclingSampleProvider(device, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsSleepMeasurement() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean supportsStepCounter() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean supportsSpeedzones() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean supportsActivityTabs() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getManufacturer() {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends Activity> getAppsManagementActivity() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRealtimeData() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBondingStyle() {
|
||||||
|
return BONDING_STYLE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||||
|
return new int[]{
|
||||||
|
R.xml.devicesettings_cycling_sensor
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||||
|
return CyclingSensorSupport.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDeviceNameResource() {
|
||||||
|
return R.string.devicetype_cycling_sensor;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.db;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import de.greenrobot.dao.AbstractDao;
|
||||||
|
import de.greenrobot.dao.Property;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSampleDao;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
|
||||||
|
public class CyclingSampleProvider extends AbstractTimeSampleProvider<CyclingSample> {
|
||||||
|
public CyclingSampleProvider(GBDevice device, DaoSession session) {
|
||||||
|
super(device, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractDao<CyclingSample, ?> getSampleDao() {
|
||||||
|
return getSession().getCyclingSampleDao();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected Property getTimestampSampleProperty() {
|
||||||
|
return CyclingSampleDao.Properties.Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected Property getDeviceIdentifierSampleProperty() {
|
||||||
|
return CyclingSampleDao.Properties.DeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CyclingSample createSample() {
|
||||||
|
return new CyclingSample();
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.casio.gbx100.CasioGBX100Devi
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGMWB5000DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGMWB5000DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGWB5600DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGWB5600DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.CmfWatchProCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.CmfWatchProCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.coordinator.CyclingSensorCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.divoom.PixooCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.divoom.PixooCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator;
|
||||||
@ -388,6 +389,7 @@ public enum DeviceType {
|
|||||||
FEMOMETER_VINCA2(FemometerVinca2DeviceCoordinator.class),
|
FEMOMETER_VINCA2(FemometerVinca2DeviceCoordinator.class),
|
||||||
PIXOO(PixooCoordinator.class),
|
PIXOO(PixooCoordinator.class),
|
||||||
SCANNABLE(ScannableDeviceCoordinator.class),
|
SCANNABLE(ScannableDeviceCoordinator.class),
|
||||||
|
CYCLING_SENSOR(CyclingSensorCoordinator.class),
|
||||||
TEST(TestDeviceCoordinator.class);
|
TEST(TestDeviceCoordinator.class);
|
||||||
|
|
||||||
private DeviceCoordinator coordinator;
|
private DeviceCoordinator coordinator;
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.cycling_sensor.support;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||||
|
|
||||||
|
public class CyclingSensorBaseSupport extends AbstractBTLEDeviceSupport {
|
||||||
|
public CyclingSensorBaseSupport(Logger logger) {
|
||||||
|
super(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean useAutoConnect() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,267 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.cycling_sensor.support;
|
||||||
|
|
||||||
|
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityKind.TYPE_CYCLING;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothGatt;
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.db.CyclingSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.NotifyAction;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
|
public class CyclingSensorSupport extends CyclingSensorBaseSupport {
|
||||||
|
static class CyclingSpeedCadenceMeasurement {
|
||||||
|
private static final int FLAG_REVOLUTION_DATA_PRESENT = 1 << 0;
|
||||||
|
private static final int FLAG_CADENCE_DATA_PRESENT = 1 << 1;
|
||||||
|
public boolean revolutionDataPresent = false;
|
||||||
|
public boolean cadenceDataPresent = false;
|
||||||
|
public int revolutionCount;
|
||||||
|
public int lastRevolutionTimeTicks;
|
||||||
|
private int crankRevolutionCount;
|
||||||
|
private int lastCrankRevolutionTimeTicks;
|
||||||
|
|
||||||
|
public static CyclingSpeedCadenceMeasurement fromPayload(byte[] payload) throws RuntimeException {
|
||||||
|
if(payload.length < 7){
|
||||||
|
throw new RuntimeException("wrong payload length");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer
|
||||||
|
.wrap(payload)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
byte flags = buffer.get();
|
||||||
|
|
||||||
|
boolean revolutionDataPresent = (flags | FLAG_REVOLUTION_DATA_PRESENT) == FLAG_REVOLUTION_DATA_PRESENT;
|
||||||
|
boolean cadenceDataPresent = (flags | FLAG_CADENCE_DATA_PRESENT) == FLAG_CADENCE_DATA_PRESENT;
|
||||||
|
CyclingSpeedCadenceMeasurement result = new CyclingSpeedCadenceMeasurement();
|
||||||
|
|
||||||
|
if(revolutionDataPresent){
|
||||||
|
result.revolutionDataPresent = true;
|
||||||
|
result.revolutionCount = buffer.getInt() & 0xFFFFFFFF; // remove sign
|
||||||
|
result.lastRevolutionTimeTicks = buffer.getShort() & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(cadenceDataPresent){
|
||||||
|
result.cadenceDataPresent = true;
|
||||||
|
result.crankRevolutionCount = buffer.getInt();
|
||||||
|
result.lastCrankRevolutionTimeTicks = buffer.getShort();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("Measurement revolutions: %d, time ticks %d", revolutionCount, lastRevolutionTimeTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static UUID UUID_CYCLING_SENSOR_SERVICE =
|
||||||
|
UUID.fromString("00001816-0000-1000-8000-00805f9b34fb");
|
||||||
|
public final static UUID UUID_CYCLING_SENSOR_CSC_MEASUREMENT =
|
||||||
|
UUID.fromString("00002a5b-0000-1000-8000-00805f9b34fb");
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CyclingSensorSupport.class);
|
||||||
|
|
||||||
|
private long persistenceInterval;
|
||||||
|
private long nextPersistenceTimestamp = 0;
|
||||||
|
|
||||||
|
private float wheelCircumference;
|
||||||
|
|
||||||
|
private CyclingSpeedCadenceMeasurement lastReportedMeasurement = null;
|
||||||
|
private long lastMeasurementTime = 0;
|
||||||
|
|
||||||
|
private BluetoothGattCharacteristic batteryCharacteristic = null;
|
||||||
|
|
||||||
|
public CyclingSensorSupport() {
|
||||||
|
super(logger);
|
||||||
|
|
||||||
|
addSupportedService(UUID_CYCLING_SENSOR_SERVICE);
|
||||||
|
addSupportedService(BatteryInfoProfile.SERVICE_UUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSendConfiguration(String config) {
|
||||||
|
switch (config){
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL:
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_WHEEL_DIAMETER:
|
||||||
|
loadConfiguration();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfiguration(){
|
||||||
|
Prefs deviceSpecificPrefs = new Prefs(
|
||||||
|
GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())
|
||||||
|
);
|
||||||
|
persistenceInterval = deviceSpecificPrefs.getInt(DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL, 60) * 1000;
|
||||||
|
nextPersistenceTimestamp = 0;
|
||||||
|
|
||||||
|
float wheelDiameter = deviceSpecificPrefs.getFloat(DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_WHEEL_DIAMETER, 29);
|
||||||
|
wheelCircumference = (float)(wheelDiameter * 2.54 * Math.PI) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||||
|
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||||
|
|
||||||
|
BluetoothGattCharacteristic measurementCharacteristic =
|
||||||
|
getCharacteristic(UUID_CYCLING_SENSOR_CSC_MEASUREMENT);
|
||||||
|
|
||||||
|
builder.add(new NotifyAction(measurementCharacteristic, true));
|
||||||
|
|
||||||
|
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||||
|
batteryCharacteristic = getCharacteristic(BatteryInfoProfile.UUID_CHARACTERISTIC_BATTERY_LEVEL);
|
||||||
|
|
||||||
|
if(batteryCharacteristic != null){
|
||||||
|
builder.add(new ReadAction(batteryCharacteristic));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfiguration();
|
||||||
|
|
||||||
|
gbDevice.setFirmwareVersion("1.0.0");
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMeasurementCharacteristic(BluetoothGattCharacteristic characteristic){
|
||||||
|
byte[] value = characteristic.getValue();
|
||||||
|
if(value == null || value.length < 7){
|
||||||
|
logger.error("Measurement characteristic value length smaller than 7");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CyclingSpeedCadenceMeasurement measurement = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
measurement = CyclingSpeedCadenceMeasurement.fromPayload(value);
|
||||||
|
}catch (RuntimeException e){
|
||||||
|
// do nothing, measurement stays null
|
||||||
|
}
|
||||||
|
|
||||||
|
if(measurement == null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!measurement.revolutionDataPresent){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCyclingSpeedMeasurement(measurement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCyclingSpeedMeasurement(CyclingSpeedCadenceMeasurement currentMeasurement) {
|
||||||
|
logger.debug("Measurement " + currentMeasurement);
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
Float speed = null;
|
||||||
|
|
||||||
|
long lastMeasurementDelta = (now - lastMeasurementTime);
|
||||||
|
|
||||||
|
if(lastMeasurementDelta <= 30_000){
|
||||||
|
int ticksPassed = currentMeasurement.lastRevolutionTimeTicks - lastReportedMeasurement.lastRevolutionTimeTicks;
|
||||||
|
// every second is subdivided in 1024 ticks
|
||||||
|
int millisDelta = (int)(ticksPassed * (1000f / 1024f));
|
||||||
|
|
||||||
|
if(millisDelta > 0) {
|
||||||
|
int revolutionsDelta = currentMeasurement.revolutionCount - lastReportedMeasurement.revolutionCount;
|
||||||
|
|
||||||
|
float revolutionsPerSecond = revolutionsDelta * (1000f / millisDelta);
|
||||||
|
|
||||||
|
speed = revolutionsPerSecond * wheelCircumference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastReportedMeasurement = currentMeasurement;
|
||||||
|
lastMeasurementTime = now;
|
||||||
|
|
||||||
|
if(now < nextPersistenceTimestamp){
|
||||||
|
// too early
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPersistenceTimestamp = now + persistenceInterval;
|
||||||
|
|
||||||
|
CyclingSample sample = new CyclingSample();
|
||||||
|
|
||||||
|
if (currentMeasurement.revolutionDataPresent) {
|
||||||
|
sample.setRevolutionCount(currentMeasurement.revolutionCount);
|
||||||
|
sample.setSpeed(speed);
|
||||||
|
sample.setDistance(currentMeasurement.revolutionCount * wheelCircumference);
|
||||||
|
}
|
||||||
|
|
||||||
|
sample.setTimestamp(now);
|
||||||
|
|
||||||
|
Intent liveIntent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES);
|
||||||
|
liveIntent.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
|
||||||
|
LocalBroadcastManager.getInstance(getContext())
|
||||||
|
.sendBroadcast(liveIntent);
|
||||||
|
|
||||||
|
try(DBHandler handler = GBApplication.acquireDB()) {
|
||||||
|
DaoSession session = handler.getDaoSession();
|
||||||
|
|
||||||
|
CyclingSampleProvider sampleProvider =
|
||||||
|
new CyclingSampleProvider(getDevice(), session);
|
||||||
|
|
||||||
|
Device databaseDevice = DBHelper.getDevice(getDevice(), session);
|
||||||
|
User databaseUser = DBHelper.getUser(session);
|
||||||
|
sample.setDevice(databaseDevice);
|
||||||
|
sample.setUser(databaseUser);
|
||||||
|
|
||||||
|
sampleProvider.addSample(sample);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// throw new RuntimeException(e);
|
||||||
|
logger.error("failed adding DB cycling sample");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||||
|
byte[] value = characteristic.getValue();
|
||||||
|
|
||||||
|
if(characteristic.equals(batteryCharacteristic) && value != null && value.length == 1){
|
||||||
|
GBDeviceEventBatteryInfo info = new GBDeviceEventBatteryInfo();
|
||||||
|
info.level = characteristic.getValue()[0];
|
||||||
|
handleGBDeviceEvent(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||||
|
if(characteristic.getUuid().equals(UUID_CYCLING_SENSOR_CSC_MEASUREMENT)){
|
||||||
|
handleMeasurementCharacteristic(characteristic);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
16
app/src/main/res/layout/fragment_cycling.xml
Normal file
16
app/src/main/res/layout/fragment_cycling.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.gridlayout.widget.GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:grid="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
grid:columnCount="2">
|
||||||
|
|
||||||
|
<com.github.mikephil.charting.charts.LineChart
|
||||||
|
android:id="@+id/chart_cycling_history"
|
||||||
|
grid:layout_columnSpan="2"
|
||||||
|
grid:layout_columnWeight="1"
|
||||||
|
grid:layout_rowWeight="1">
|
||||||
|
</com.github.mikephil.charting.charts.LineChart>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.gridlayout.widget.GridLayout>
|
@ -3014,6 +3014,7 @@
|
|||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="pref_charts_tabs_items_default">
|
<string-array name="pref_charts_tabs_items_default">
|
||||||
|
<item>@string/p_cycling</item>
|
||||||
<item>@string/p_activity</item>
|
<item>@string/p_activity</item>
|
||||||
<item>@string/p_activity_list</item>
|
<item>@string/p_activity_list</item>
|
||||||
<item>@string/p_sleep</item>
|
<item>@string/p_sleep</item>
|
||||||
|
@ -2832,4 +2832,14 @@
|
|||||||
<string name="pref_title_huawei_account">Huawei Account</string>
|
<string name="pref_title_huawei_account">Huawei Account</string>
|
||||||
<string name="pref_summary_huawei_account">Huawei account used in pairing process. Setting it allows to pair without factory reset.</string>
|
<string name="pref_summary_huawei_account">Huawei account used in pairing process. Setting it allows to pair without factory reset.</string>
|
||||||
<string name="watchface_resolution_doesnt_match">Watchface resolution doesnt match device screen. Watchface is %1$s device screen is %2$s</string>
|
<string name="watchface_resolution_doesnt_match">Watchface resolution doesnt match device screen. Watchface is %1$s device screen is %2$s</string>
|
||||||
|
<string name="device_name_bicycle_sensor">Bicycle sensor</string>
|
||||||
|
<string name="device_name_cycling_sensor">Cycling sensor</string>
|
||||||
|
<string name="devicetype_cycling_sensor">Cycling speed sensor</string>
|
||||||
|
<string name="title_cycling">Cycling</string>
|
||||||
|
<string name="pref_summary_wheel_diameter">Wheel diameter in inches. Typically 29, 27,5 or 26.</string>
|
||||||
|
<string name="pref_title_wheel_diameter">Wheel diameter</string>
|
||||||
|
<string name="pref_summary_cycling_persistence_interval">Interval in seconds when the current cycling data should be written to the database</string>
|
||||||
|
<string name="pref_title_cycling_persistence_interval">Persistence interval</string>
|
||||||
|
<string name="chart_cycling_point_label_distance">Today: %.1f km\nTotal: %.1f km</string>
|
||||||
|
<string name="chart_cycling_point_label_speed">%.1f km/h</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -96,6 +96,7 @@
|
|||||||
<item name="p_call_privacy_mode_number" type="string">number</item>
|
<item name="p_call_privacy_mode_number" type="string">number</item>
|
||||||
<item name="p_call_privacy_mode_complete" type="string">complete</item>
|
<item name="p_call_privacy_mode_complete" type="string">complete</item>
|
||||||
|
|
||||||
|
<item name="p_cycling" type="string">cycling</item>
|
||||||
<item name="p_activity" type="string">activity</item>
|
<item name="p_activity" type="string">activity</item>
|
||||||
<item name="p_activity_list" type="string">activitylist</item>
|
<item name="p_activity_list" type="string">activitylist</item>
|
||||||
<item name="p_sleep" type="string">sleep</item>
|
<item name="p_sleep" type="string">sleep</item>
|
||||||
|
21
app/src/main/res/xml/devicesettings_cycling_sensor.xml
Normal file
21
app/src/main/res/xml/devicesettings_cycling_sensor.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.preference.PreferenceScreen
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:defaultValue="29"
|
||||||
|
android:inputType="number"
|
||||||
|
android:title="@string/pref_title_wheel_diameter"
|
||||||
|
android:summary="@string/pref_summary_wheel_diameter"
|
||||||
|
android:icon="@drawable/ic_activity_biking"
|
||||||
|
android:key="pref_cycling_wheel_diameter" />
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:defaultValue="60"
|
||||||
|
android:inputType="number"
|
||||||
|
android:title="@string/pref_title_cycling_persistence_interval"
|
||||||
|
android:summary="@string/pref_summary_cycling_persistence_interval"
|
||||||
|
android:icon="@drawable/ic_activity_biking"
|
||||||
|
android:key="pref_cycling_persistence_interval" />
|
||||||
|
|
||||||
|
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user