From cb5518a261692f16aac61ba7471b179aff12aebb Mon Sep 17 00:00:00 2001 From: Dominik R?ttsches Date: Sat, 20 Aug 2011 01:01:43 +0300 Subject: [PATCH 1/3] Adding battery_level to protobuffers spec. --- MyTracksLib/proto/sensor.proto | 1 + .../android/apps/mytracks/content/Sensor.java | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/MyTracksLib/proto/sensor.proto b/MyTracksLib/proto/sensor.proto index b0ea1ac11..a3261d7f7 100644 --- a/MyTracksLib/proto/sensor.proto +++ b/MyTracksLib/proto/sensor.proto @@ -50,4 +50,5 @@ message SensorDataSet { optional SensorData heart_rate = 2; optional SensorData cadence = 3; optional SensorData power = 4; + optional SensorData battery_level = 5; } \ No newline at end of file diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java index 54a9df31d..f564c75f8 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java @@ -398,10 +398,18 @@ public final class Sensor { public boolean hasPower() { return hasPower; } public com.google.android.apps.mytracks.content.Sensor.SensorData getPower() { return power_; } + // optional .com.google.android.apps.mytracks.content.SensorData battery_level = 5; + public static final int BATTERY_LEVEL_FIELD_NUMBER = 5; + private boolean hasBatteryLevel; + private com.google.android.apps.mytracks.content.Sensor.SensorData batteryLevel_; + public boolean hasBatteryLevel() { return hasBatteryLevel; } + public com.google.android.apps.mytracks.content.Sensor.SensorData getBatteryLevel() { return batteryLevel_; } + private void initFields() { heartRate_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); cadence_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); power_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + batteryLevel_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); } public final boolean isInitialized() { if (hasHeartRate()) { @@ -413,6 +421,9 @@ public final class Sensor { if (hasPower()) { if (!getPower().isInitialized()) return false; } + if (hasBatteryLevel()) { + if (!getBatteryLevel().isInitialized()) return false; + } return true; } @@ -431,6 +442,9 @@ public final class Sensor { if (hasPower()) { output.writeMessage(4, getPower()); } + if (hasBatteryLevel()) { + output.writeMessage(5, getBatteryLevel()); + } } private int memoizedSerializedSize = -1; @@ -455,6 +469,10 @@ public final class Sensor { size += com.google.protobuf.CodedOutputStream .computeMessageSize(4, getPower()); } + if (hasBatteryLevel()) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(5, getBatteryLevel()); + } memoizedSerializedSize = size; return size; } @@ -611,6 +629,9 @@ public final class Sensor { if (other.hasPower()) { mergePower(other.getPower()); } + if (other.hasBatteryLevel()) { + mergeBatteryLevel(other.getBatteryLevel()); + } return this; } @@ -660,6 +681,15 @@ public final class Sensor { setPower(subBuilder.buildPartial()); break; } + case 42: { + com.google.android.apps.mytracks.content.Sensor.SensorData.Builder subBuilder = com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(); + if (hasBatteryLevel()) { + subBuilder.mergeFrom(getBatteryLevel()); + } + input.readMessage(subBuilder, extensionRegistry); + setBatteryLevel(subBuilder.buildPartial()); + break; + } } } } @@ -794,6 +824,43 @@ public final class Sensor { return this; } + // optional .com.google.android.apps.mytracks.content.SensorData battery_level = 5; + public boolean hasBatteryLevel() { + return result.hasBatteryLevel(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorData getBatteryLevel() { + return result.getBatteryLevel(); + } + public Builder setBatteryLevel(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (value == null) { + throw new NullPointerException(); + } + result.hasBatteryLevel = true; + result.batteryLevel_ = value; + return this; + } + public Builder setBatteryLevel(com.google.android.apps.mytracks.content.Sensor.SensorData.Builder builderForValue) { + result.hasBatteryLevel = true; + result.batteryLevel_ = builderForValue.build(); + return this; + } + public Builder mergeBatteryLevel(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (result.hasBatteryLevel() && + result.batteryLevel_ != com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance()) { + result.batteryLevel_ = + com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(result.batteryLevel_).mergeFrom(value).buildPartial(); + } else { + result.batteryLevel_ = value; + } + result.hasBatteryLevel = true; + return this; + } + public Builder clearBatteryLevel() { + result.hasBatteryLevel = false; + result.batteryLevel_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + return this; + } + // @@protoc_insertion_point(builder_scope:com.google.android.apps.mytracks.content.SensorDataSet) } From d9725240a67f8ff76ad9fa929c68bb359e240e37 Mon Sep 17 00:00:00 2001 From: Dominik R?ttsches Date: Sat, 20 Aug 2011 01:25:58 +0300 Subject: [PATCH 2/3] Storing battery level in new protobuf field. --- .../mytracks/services/sensors/ZephyrMessageParser.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java index 48fd5daa0..def1ba3dc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java @@ -15,11 +15,11 @@ */ package com.google.android.apps.mytracks.services.sensors; -import android.util.Log; - import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; +import android.util.Log; + /** * An implementation of a Sensor MessageParser for Zephyr. * @@ -42,8 +42,7 @@ public class ZephyrMessageParser implements MessageParser { Sensor.SensorData.Builder heartrate = Sensor.SensorData.newBuilder() .setValue(buffer[12] & 0xFF) .setState(Sensor.SensorState.SENDING); - // Changes Nico Laum (Power and Cadence) - Sensor.SensorData.Builder power = Sensor.SensorData.newBuilder() + Sensor.SensorData.Builder batteryLevel = Sensor.SensorData.newBuilder() .setValue(buffer[11]) .setState(Sensor.SensorState.SENDING); Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder() @@ -53,7 +52,7 @@ public class ZephyrMessageParser implements MessageParser { Sensor.SensorDataSet sds = Sensor.SensorDataSet.newBuilder() .setCreationTime(System.currentTimeMillis()) - .setPower(power) + .setBatteryLevel(batteryLevel) .setHeartRate(heartrate) .setCadence(cadence) .build(); From 49033bf9529ad09f9a2f9118d1dd92998e2955de Mon Sep 17 00:00:00 2001 From: Dominik R?ttsches Date: Sat, 20 Aug 2011 16:58:36 +0300 Subject: [PATCH 3/3] Fix for Cadence values from HxM with Hardware/Firmware Identification 0x1A00316550003162 --- .../services/sensors/ZephyrMessageParser.java | 99 +++++++++++++++++-- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java index def1ba3dc..4f641734b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java @@ -1,5 +1,6 @@ /* * Copyright 2010 Google Inc. + * Copyright 2011 Dominik Ršttsches * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -20,6 +21,8 @@ import com.google.android.apps.mytracks.content.Sensor; import android.util.Log; +import java.util.LinkedList; + /** * An implementation of a Sensor MessageParser for Zephyr. * @@ -30,7 +33,55 @@ public class ZephyrMessageParser implements MessageParser { public static final int ZEPHYR_HXM_BYTE_STX = 0; public static final int ZEPHYR_HXM_BYTE_CRC = 58; public static final int ZEPHYR_HXM_BYTE_ETX = 59; - + + private StrideReadings strideReadings; + + public class StrideReadings { + class StrideReading { + public int timeMs; + public int numStrides; + + StrideReading(int newTimeMs, int newNumStrides) { + timeMs = newTimeMs; + numStrides = newNumStrides; + } + } + + private LinkedList strideReadingsHistory; + private static final int NUM_READINGS_FOR_AVERAGE = 10; + private static final int MIN_READINGS_FOR_AVERAGE = 5; + public static final int CADENCE_NOT_AVAILABLE = -1; + + public StrideReadings() { + strideReadingsHistory = new LinkedList(); + } + + public void updateStrideReading(int timeInMs, int numStrides) { + // HRM/HxM docs say, transmission frequency is 1 Hz, + // let's keep last NUM_READINGS_FOR_AVERAGE readings. + // TODO: Calibrate this using a reliable footpod / cadence sensor, + // otherwise perhaps use heartbeat timestamp for calculation. + strideReadingsHistory.addFirst(new StrideReading(timeInMs, numStrides)); + while(strideReadingsHistory.size() > NUM_READINGS_FOR_AVERAGE) { + strideReadingsHistory.removeLast(); + } + } + + public int getCadence() { + if(strideReadingsHistory.size() < MIN_READINGS_FOR_AVERAGE) { + // Bail out if we cannot really get a meaningful average yet. + return CADENCE_NOT_AVAILABLE; + } + // compute assuming 1 stride reading/second + int timeSinceOldestReadingSecs = strideReadingsHistory.size() - 1; + int stridesThen = strideReadingsHistory.getLast().numStrides; + int stridesNow = strideReadingsHistory.getFirst().numStrides; + // Contrary to documentation stride value seems to roll over every 127 strides. + return Math.round( (float)((stridesNow - stridesThen) % 127) / + timeSinceOldestReadingSecs * 60); + } + } + @Override public Sensor.SensorDataSet parseBuffer(byte[] buffer) { StringBuilder sb = new StringBuilder(); @@ -38,17 +89,45 @@ public class ZephyrMessageParser implements MessageParser { sb.append(String.format("%02X", buffer[i])); } Log.w(Constants.TAG, "Got zephyr data: " + sb); - // Heart Rate + + // Device Firmware ID, Firmware Version, Hardware ID, Hardware Version + // 0x1A00316550003162 produces erroneous values for Cadence and needs + // a workaround based on the stride counter. + String hardwareFirmwareId = sb.substring(6, 22); + boolean needsWorkaround = hardwareFirmwareId.equals("1A00316550003162"); + Log.w(Constants.TAG, "FW & HW Ids & Version " + hardwareFirmwareId + " needs workaround: " + needsWorkaround); + Sensor.SensorData.Builder heartrate = Sensor.SensorData.newBuilder() - .setValue(buffer[12] & 0xFF) - .setState(Sensor.SensorState.SENDING); - Sensor.SensorData.Builder batteryLevel = Sensor.SensorData.newBuilder() - .setValue(buffer[11]) - .setState(Sensor.SensorState.SENDING); - Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder() - .setValue(SensorUtils.unsignedShortToIntLittleEndian(buffer, 56) / 16) - .setState(Sensor.SensorState.SENDING); + .setValue(buffer[12] & 0xFF) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder batteryLevel = Sensor.SensorData.newBuilder() + .setValue(buffer[11]) + .setState(Sensor.SensorState.SENDING); + + Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder(); + + if(!needsWorkaround) { + cadence = cadence + .setValue(SensorUtils.unsignedShortToIntLittleEndian(buffer, 56) / 16) + .setState(Sensor.SensorState.SENDING); + } else { + if(strideReadings == null) { + strideReadings = new StrideReadings(); + } + + strideReadings.updateStrideReading( + SensorUtils.unsignedShortToIntLittleEndian(buffer, 14), + buffer[54] & 0xFF); + + if(strideReadings.getCadence() != StrideReadings.CADENCE_NOT_AVAILABLE) { + cadence = cadence.setValue(strideReadings.getCadence()) + .setState(Sensor.SensorState.SENDING); + } else { + cadence = cadence.setValue(0).setState(Sensor.SensorState.NONE); + } + } + Sensor.SensorDataSet sds = Sensor.SensorDataSet.newBuilder() .setCreationTime(System.currentTimeMillis())