diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java index 28f2f4fb8..e52103f23 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java @@ -24,14 +24,16 @@ import com.google.android.maps.mytracks.R; * A collection of methods for message parsers. * * @author Sandor Dornbush + * @author Nico Laum */ public class SensorUtils { private SensorUtils() { } - + /** * Extract one unsigned short from a big endian byte array. + * * @param buffer the buffer to extract the short from * @param index the first byte to be interpreted as part of the short * @return The unsigned short at the given index in the buffer @@ -42,6 +44,58 @@ public class SensorUtils { return r; } + /** + * Extract one unsigned short from a little endian byte array. + * + * @param buffer the buffer to extract the short from + * @param index the first byte to be interpreted as part of the short + * @return The unsigned short at the given index in the buffer + */ + public static int unsignedShortToIntLittleEndian(byte[] buffer, int index) { + int r = buffer[index] & 0xFF; + r |= (buffer[index + 1] & 0xFF) << 8; + return r; + } + + /** + * Returns CRC8 (polynomial 0x8C) from byte array buffer[start] until + * (including) buffer[end] + * + * @param buffer the byte array of data (payload) + * @param start the position in the byte array where the payload begins + * @param end the position in the byte array where the payload ends + * @return CRC8 value + */ + public static byte getCrc8(byte[] buffer, int start, int length) { + byte crc = 0x0; + + for (int i = start; i < (start + length); i++) { + crc = crc8PushByte(crc, buffer[i]); + } + return crc; + } + + /** + * Updates a CRC8 value by using the next byte passed to this method + * + * @param crc int of crc value + * @param add the next byte to add to the CRC8 calculation + */ + private static byte crc8PushByte(byte crc, byte add) { + crc = (byte) (crc ^ add); + + for (int i = 0; i < 8; i++) { + if ((crc & 0x1) != 0x0) { + // Using a 0xFF bit assures that 0-bits are introduced during the shift operation. + // Otherwise, implicit casts to signed int could shift in 1-bits if the signed bit is 1. + crc = (byte) (((crc & 0xFF) >> 1) ^ 0x8C); + } else { + crc = (byte) ((crc & 0xFF) >> 1); + } + } + return crc; + } + public static String getStateAsString(Sensor.SensorState state, Context c) { switch (state) { case NONE: 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 da7a3672e..17c4ae1b3 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 @@ -21,12 +21,16 @@ import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; /** - * An implementation of a SensorData parser for Zephyr HRM. + * An implementation of a Sensor MessageParser for Zephyr. * * @author Sandor Dornbush */ 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; + @Override public Sensor.SensorDataSet parseBuffer(byte[] buffer) { StringBuilder sb = new StringBuilder(); @@ -34,20 +38,24 @@ public class ZephyrMessageParser implements MessageParser { sb.append(String.format("%02X", buffer[i])); } Log.w(Constants.TAG, "Got zephyr data: " + sb); - // The provided units are 1/16 strides per minute. - // TODO: Fix the cadence calculation. - // int cadence = SensorUtils.unsignedShortToInt(buffer, 56); // Heart Rate - Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder() - .setValue(buffer[12] & 0xFF) - .setState(Sensor.SensorState.SENDING); - // Cadence - //.setCadence(cadence / 16) - //.build(); + Sensor.SensorData.Builder heartrate = Sensor.SensorData.newBuilder() + .setValue(buffer[12]) + .setState(Sensor.SensorState.SENDING); + // Changes Nico Laum (Power and Cadence) + Sensor.SensorData.Builder power = 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); + Sensor.SensorDataSet sds = Sensor.SensorDataSet.newBuilder() .setCreationTime(System.currentTimeMillis()) - .setHeartRate(b) + .setPower(power) + .setHeartRate(heartrate) + .setCadence(cadence) .build(); return sds; @@ -55,8 +63,11 @@ public class ZephyrMessageParser implements MessageParser { @Override public boolean isValid(byte[] buffer) { - // TODO crc etc. - return buffer[0] == 0x02 && buffer[59] == 0x03; + // Check STX (Start of Text), ETX (End of Text) and CRC Checksum + return buffer.length > ZEPHYR_HXM_BYTE_ETX + && buffer[ZEPHYR_HXM_BYTE_STX] == 0x02 + && buffer[ZEPHYR_HXM_BYTE_ETX] == 0x03 + && SensorUtils.getCrc8(buffer, 3, 55) == buffer[ZEPHYR_HXM_BYTE_CRC]; } @Override diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java index 6cebf3692..11a08fe49 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java @@ -9,10 +9,17 @@ public class ZephyrMessageParserTest extends TestCase { ZephyrMessageParser parser = new ZephyrMessageParser(); public void testIsValid() { - byte[] buf = new byte[60]; + byte[] smallBuf = new byte[59]; + assertFalse(parser.isValid(smallBuf)); + // A complete and valid Zephyr HxM packet + byte[] buf = { 2,38,55,26,0,49,101,80,0,49,98,100,42,113,120,-53,-24,-60,-123,-61,117,-69,42,-75,74,-78,51,-79,27,-83,28,-88,28,-93,29,-98,25,-103,26,-108,26,-113,59,-118,0,0,0,0,0,0,-22,3,125,1,48,0,96,4,30,0 }; + // Make buffer invalid + buf[0] = buf[58] = buf[59] = 0; assertFalse(parser.isValid(buf)); buf[0] = 0x02; assertFalse(parser.isValid(buf)); + buf[58] = 0x1E; + assertFalse(parser.isValid(buf)); buf[59] = 0x03; assertTrue(parser.isValid(buf)); } @@ -20,10 +27,19 @@ public class ZephyrMessageParserTest extends TestCase { public void testParseBuffer() { byte[] buf = new byte[60]; buf[12] = 50; + buf[11] = 51; + buf[56] = 32; + buf[57] = 0; Sensor.SensorDataSet sds = parser.parseBuffer(buf); assertTrue(sds.hasHeartRate()); assertTrue(sds.getHeartRate().getState() == Sensor.SensorState.SENDING); assertEquals(50, sds.getHeartRate().getValue()); + assertTrue(sds.hasPower()); + assertTrue(sds.getPower().getState() == Sensor.SensorState.SENDING); + assertEquals(51, sds.getPower().getValue()); + assertTrue(sds.hasCadence()); + assertTrue(sds.getCadence().getState() == Sensor.SensorState.SENDING); + assertEquals(2, sds.getCadence().getValue()); } public void testFindNextAlignment() {