/* * This source is part of the * _____ ___ ____ * __ / / _ \/ _ | / __/___ _______ _ * / // / , _/ __ |/ _/_/ _ \/ __/ _ `/ * \___/_/|_/_/ |_/_/ (_)___/_/ \_, / * /___/ * repository. * * Copyright (C) 2014 Benoit 'BoD' Lubek (BoD@JRAF.org) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.jraf.android.bikey.backend.cadence; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import org.jraf.android.bikey.app.Application; import org.jraf.android.util.listeners.Listeners; import org.jraf.android.util.log.Log; import org.jraf.android.util.math.MathUtil; import org.jraf.android.util.object.ObjectUtil; public class CadenceManager { private static final CadenceManager INSTANCE = new CadenceManager(); private static final long BROADCAST_CURRENT_VALUE_RATE_S = 2; protected static final long LOG_SIZE_MS = 5 * 1000; private static final float SANITY_CHECK_MAX = 170; private static final float SANITY_CHECK_MIN = 30; private static final float MIN_AMPLITUDE = .3f; private static class Entry { long timestamp; float[] values; Entry(float[] values) { this.values = values; timestamp = System.currentTimeMillis(); } } public static CadenceManager get() { return INSTANCE; } private Context mContext; private ArrayDeque<Entry> mValues = new ArrayDeque<>(200); private ScheduledExecutorService mScheduledExecutorService; protected Float mLastValue = -1f; private float[][] mLastRawData; private Listeners<CadenceListener> mListeners = new Listeners<CadenceListener>() { @Override protected void onFirstListener() { startListening(); } @Override protected void onNoMoreListeners() { stopListening(); } }; private CadenceManager() { mContext = Application.getApplication(); } public void addListener(CadenceListener listener) { mListeners.add(listener); } public void removeListener(CadenceListener listener) { mListeners.remove(listener); } protected void startListening() { Log.d(); SensorManager sensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); sensorManager.registerListener(mRotationSensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR), SensorManager.SENSOR_DELAY_NORMAL); if (mScheduledExecutorService == null) { mScheduledExecutorService = Executors.newScheduledThreadPool(1); } mScheduledExecutorService.scheduleAtFixedRate(mBroadcastCurrentValueRunnable, BROADCAST_CURRENT_VALUE_RATE_S, BROADCAST_CURRENT_VALUE_RATE_S, TimeUnit.SECONDS); } protected void stopListening() { Log.d(); SensorManager sensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); sensorManager.unregisterListener(mRotationSensorEventListener); if (mScheduledExecutorService != null) { mScheduledExecutorService.shutdown(); mScheduledExecutorService = null; } } private SensorEventListener mRotationSensorEventListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { // Keep only logs for a specific duration (discard old logs) while (mValues.size() >= 2 && mValues.peekLast().timestamp - mValues.peekFirst().timestamp >= LOG_SIZE_MS) { mValues.removeFirst(); } float[] values = event.values.clone(); synchronized (mValues) { mValues.add(new Entry(values)); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {} }; private static class ValuesAsArray { float[][] values; long[] times; } private ValuesAsArray getValuesAsFloatArray() { float[][] values = new float[3][mValues.size()]; long[] timestamps = new long[mValues.size()]; int i = 0; for (Entry e : mValues) { float valX = e.values[0]; float valY = e.values[1]; float valZ = e.values[2]; values[0][i] = valX; values[1][i] = valY; values[2][i] = valZ; timestamps[i] = e.timestamp; i++; } ValuesAsArray res = new ValuesAsArray(); res.values = values; res.times = timestamps; return res; } /** * Get the current cadence. * * @return The current cadence in revolutions per minute, or {@code null} if the information is not available. */ private Float getCurrentCadence() { if (mListeners.size() == 0) throw new IllegalStateException("There must be at least one listener prior to calling getCurrentCadence"); ValuesAsArray valuesAsArrays; long durationMs; int len; synchronized (mValues) { len = mValues.size(); if (len < 2) return null; valuesAsArrays = getValuesAsFloatArray(); durationMs = mValues.peekLast().timestamp - mValues.peekFirst().timestamp; } mLastRawData = valuesAsArrays.values; float[] vX = valuesAsArrays.values[0]; float[] vY = valuesAsArrays.values[1]; float[] vZ = valuesAsArrays.values[2]; float amplitudeX = MathUtil.getAmplitude(vX); float amplitudeY = MathUtil.getAmplitude(vY); float amplitudeZ = MathUtil.getAmplitude(vZ); // Use the values with the highest amplitude float maxAmplitude = MathUtil.getMinMax(amplitudeX, amplitudeY, amplitudeZ)[1]; if (maxAmplitude < MIN_AMPLITUDE) { Log.d("Amplitude to small: returning null"); return null; } float[] values; String logValues; if (maxAmplitude == amplitudeX) { values = vX; logValues = "x"; } else if (maxAmplitude == amplitudeY) { values = vY; logValues = "y"; } else { values = vZ; logValues = "z"; } // Average float average = MathUtil.getAverage(values); ArrayList<Long> periods = new ArrayList<>(); long lastTime = -1; for (int i = 1; i < len; i++) { if (values[i - 1] < average && values[i] >= average) { // Going up if (lastTime != -1) { long duration = valuesAsArrays.times[i] - lastTime; periods.add(duration); } lastTime = valuesAsArrays.times[i]; } } int periodCount = periods.size(); if (periodCount == 0) { Log.d("No periods: returning null"); return null; } // Average of the rev per ms for each period float revPerMs = 0; for (Long t : periods) { revPerMs += 1f / t; } revPerMs /= periodCount; float revPerMin = revPerMs * 60000f; Log.d("durationMs=" + durationMs + " revPerMin=" + revPerMin + " periods=" + periods + " values=" + logValues + " maxAmplitude=" + maxAmplitude); // Sanity checks if (revPerMin > SANITY_CHECK_MAX || revPerMin < SANITY_CHECK_MIN) { Log.d("Invalid value " + revPerMin + ": returning null"); return null; } return revPerMin; } private Runnable mBroadcastCurrentValueRunnable = new Runnable() { @Override public void run() { Float value = getCurrentCadence(); if (ObjectUtil.equals(mLastValue, value)) { // Skip if the value was the same mLastValue = value; return; } mLastValue = value; mListeners.dispatch(listener -> listener.onCadenceChanged(value, mLastRawData)); } }; }