Garmin Vivomove HR support

- communication protocols
- device support implementation
- download FIT file storage

Features:
- basic connectivity: time sync, battery status, HW/FW version info
- real-time activity tracking
- fitness data sync
- find the device, find the phone
- factory reset

Features implemented but not working:
- notifications: fully implemented, seem to communicate correctly, but not shown on watch

Features implemented partially (not expected to work now):
- weather information (and in future possibly weather alerts)
- music info
- firmware update: only the initial file upload implemented, not used

Things to improve/change:
- Device name hardcoded in `VivomoveHrCoordinator.getSupportedType`, service UUIDs not available
- Download FIT file storage: Should be store (and offer the user to export?) the FIT data forever?
- Obviously, various code improvements, cleanup, etc.
This commit is contained in:
Mormegil 2023-06-15 17:47:42 +02:00 committed by José Rebelo
parent 114f6fcbf0
commit 3a58314db6
112 changed files with 6998 additions and 11 deletions

View File

@ -92,6 +92,7 @@ public class GBDaoGenerator {
addPineTimeActivitySample(schema, user, device);
addHybridHRActivitySample(schema, user, device);
addVivomoveHrActivitySample(schema, user, device);
addDownloadedFitFile(schema, user, device);
addCalendarSyncState(schema, device);
addAlarms(schema, user, device);
@ -475,6 +476,35 @@ public class GBDaoGenerator {
return activitySample;
}
private static Entity addDownloadedFitFile(Schema schema, Entity user, Entity device) {
final Entity downloadedFitFile = addEntity(schema, "DownloadedFitFile");
downloadedFitFile.implementsSerializable();
downloadedFitFile.setJavaDoc("This class represents a single FIT file downloaded from a FIT-compatible device.");
downloadedFitFile.addIdProperty().autoincrement();
downloadedFitFile.addLongProperty("downloadTimestamp").notNull();
final Property deviceId = downloadedFitFile.addLongProperty("deviceId").notNull().getProperty();
downloadedFitFile.addToOne(device, deviceId);
final Property userId = downloadedFitFile.addLongProperty("userId").notNull().getProperty();
downloadedFitFile.addToOne(user, userId);
final Property fileNumber = downloadedFitFile.addIntProperty("fileNumber").notNull().getProperty();
downloadedFitFile.addIntProperty("fileDataType").notNull();
downloadedFitFile.addIntProperty("fileSubType").notNull();
downloadedFitFile.addLongProperty("fileTimestamp").notNull();
downloadedFitFile.addIntProperty("specificFlags").notNull();
downloadedFitFile.addIntProperty("fileSize").notNull();
downloadedFitFile.addByteArrayProperty("fileData");
final Index indexUnique = new Index();
indexUnique.addProperty(deviceId);
indexUnique.addProperty(userId);
indexUnique.addProperty(fileNumber);
indexUnique.makeUnique();
downloadedFitFile.addIndex(indexUnique);
return downloadedFitFile;
}
private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "WatchXPlusActivitySample");
activitySample.implementsSerializable();

View File

@ -63,7 +63,7 @@ public class VivomoveHrCoordinator extends AbstractDeviceCoordinator {
}
@Override
public int getAlarmSlotCount() {
public int getAlarmSlotCount(GBDevice device) {
return 0;
}

View File

@ -81,6 +81,17 @@ import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static nodomain.freeyourgadget.gadgetbridge.activities.NotificationFilterActivity.NOTIFICATION_FILTER_MODE_BLACKLIST;
import static nodomain.freeyourgadget.gadgetbridge.activities.NotificationFilterActivity.NOTIFICATION_FILTER_MODE_WHITELIST;
@ -333,6 +344,7 @@ public class NotificationListener extends NotificationListenerService {
}
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.when = notification.when;
// determinate Source App Name ("Label")
String name = getAppName(source);

View File

@ -24,6 +24,7 @@ public class NotificationSpec {
public int flags;
private static final AtomicInteger c = new AtomicInteger((int) (System.currentTimeMillis()/1000));
private int id;
public long when;
public String sender;
public String phoneNumber;
public String title;
@ -53,7 +54,7 @@ public class NotificationSpec {
public int dndSuppressed;
public NotificationSpec() {
this.id = c.incrementAndGet();
this(-1);
}
public NotificationSpec(int id) {
@ -61,6 +62,7 @@ public class NotificationSpec {
this.id = id;
else
this.id = c.incrementAndGet();
this.when = System.currentTimeMillis();
}
public int getId() {

View File

@ -25,25 +25,20 @@ package nodomain.freeyourgadget.gadgetbridge.service;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.widget.Toast;
import java.lang.reflect.Constructor;
import java.util.EnumSet;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.asteroidos.AsteroidOSDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.binary_sensor.BinarySensorSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.fitpro.FitProDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.binary_sensor.BinarySensorSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.CasioGB6900DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casio.CasioGBX100DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.domyos.DomyosT540Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.fitpro.FitProDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.flipper.zero.support.FlipperZeroSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.galaxy_buds.GalaxyBudsDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitband5.AmazfitBand5Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitband7.AmazfitBand7Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipLiteSupport;
@ -112,12 +107,15 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.tlw64.TLW64Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.um25.Support.UM25Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vesc.VescDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.VivomoveHrSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.waspos.WaspOSDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import java.lang.reflect.Constructor;
public class DeviceSupportFactory {
private final BluetoothAdapter mBtAdapter;
private final Context mContext;
@ -372,6 +370,8 @@ public class DeviceSupportFactory {
return new ServiceDeviceSupport(new AsteroidOSDeviceSupport());
case SOFLOW_SO6:
return new ServiceDeviceSupport(new SoFlowSupport());
case VIVOMOVE_HR:
return new ServiceDeviceSupport(new VivomoveHrSupport());
}
return null;
}

View File

@ -0,0 +1,50 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
public final class BinaryUtils {
private BinaryUtils() {
}
public static int readByte(byte[] array, int offset) {
return array[offset] & 0xFF;
}
public static int readShort(byte[] array, int offset) {
return (array[offset] & 0xFF) | ((array[offset + 1] & 0xFF) << 8);
}
public static int readInt(byte[] array, int offset) {
return (array[offset] & 0xFF) | ((array[offset + 1] & 0xFF) << 8) | ((array[offset + 2] & 0xFF) << 16) | ((array[offset + 3] & 0xFF) << 24);
}
public static long readLong(byte[] array, int offset) {
return (array[offset] & 0xFFL) | ((array[offset + 1] & 0xFFL) << 8) | ((array[offset + 2] & 0xFFL) << 16) | ((array[offset + 3] & 0xFFL) << 24) |
((array[offset + 4] & 0xFFL) << 32) | ((array[offset + 5] & 0xFFL) << 40) | ((array[offset + 6] & 0xFFL) << 48) | ((array[offset + 7] & 0xFFL) << 56);
}
public static void writeByte(byte[] array, int offset, int value) {
array[offset] = (byte) value;
}
public static void writeShort(byte[] array, int offset, int value) {
array[offset] = (byte) value;
array[offset + 1] = (byte) (value >> 8);
}
public static void writeInt(byte[] array, int offset, int value) {
array[offset] = (byte) value;
array[offset + 1] = (byte) (value >> 8);
array[offset + 2] = (byte) (value >> 16);
array[offset + 3] = (byte) (value >> 24);
}
public static void writeLong(byte[] array, int offset, long value) {
array[offset] = (byte) value;
array[offset + 1] = (byte) (value >> 8);
array[offset + 2] = (byte) (value >> 16);
array[offset + 3] = (byte) (value >> 24);
array[offset + 4] = (byte) (value >> 32);
array[offset + 5] = (byte) (value >> 40);
array[offset + 6] = (byte) (value >> 48);
array[offset + 7] = (byte) (value >> 56);
}
}

View File

@ -0,0 +1,22 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
public final class ChecksumCalculator {
private static final int[] CONSTANTS = {0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400};
private ChecksumCalculator() {
}
public static int computeCrc(byte[] data, int offset, int length) {
return computeCrc(0, data, offset, length);
}
public static int computeCrc(int initialCrc, byte[] data, int offset, int length) {
int crc = initialCrc;
for (int i = offset; i < offset + length; ++i) {
int b = data[i];
crc = (((crc >> 4) & 4095) ^ CONSTANTS[crc & 15]) ^ CONSTANTS[b & 15];
crc = (((crc >> 4) & 4095) ^ CONSTANTS[crc & 15]) ^ CONSTANTS[(b >> 4) & 15];
}
return crc;
}
}

View File

@ -0,0 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
public enum GarminDeviceSetting {
DEVICE_NAME,
CURRENT_TIME,
DAYLIGHT_SAVINGS_TIME_OFFSET,
TIME_ZONE_OFFSET,
NEXT_DAYLIGHT_SAVINGS_START,
NEXT_DAYLIGHT_SAVINGS_END,
AUTO_UPLOAD_ENABLED,
WEATHER_CONDITIONS_ENABLED,
WEATHER_ALERTS_ENABLED;
}

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
public enum GarminMessageType {
SCHEDULES,
SETTINGS,
GOALS,
WORKOUTS,
COURSES,
ACTIVITIES,
PERSONAL_RECORDS,
UNKNOWN_TYPE,
SOFTWARE_UPDATE,
DEVICE_SETTINGS,
LANGUAGE_SETTINGS,
USER_PROFILE,
SPORTS,
SEGMENT_LEADERS,
GOLF_CLUB,
WELLNESS_DEVICE_INFO,
WELLNESS_DEVICE_CCF,
INSTALL_APP,
CHECK_BACK,
TRUE_UP,
SETTINGS_CHANGE,
ACTIVITY_SUMMARY,
METRICS_FILE,
PACE_BAND
}

View File

@ -0,0 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
public enum GarminMusicControlCommand {
TOGGLE_PLAY_PAUSE,
SKIP_TO_NEXT_ITEM,
SKIP_TO_PREVIOUS_ITEM,
VOLUME_UP,
VOLUME_DOWN,
PLAY,
PAUSE,
SKIP_FORWARD,
SKIP_BACKWARDS;
}

View File

@ -0,0 +1,21 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
public enum GarminSystemEventType {
SYNC_COMPLETE,
SYNC_FAIL,
FACTORY_RESET,
PAIR_START,
PAIR_COMPLETE,
PAIR_FAIL,
HOST_DID_ENTER_FOREGROUND,
HOST_DID_ENTER_BACKGROUND,
SYNC_READY,
NEW_DOWNLOAD_AVAILABLE,
DEVICE_SOFTWARE_UPDATE,
DEVICE_DISCONNECT,
TUTORIAL_COMPLETE,
SETUP_WIZARD_START,
SETUP_WIZARD_COMPLETE,
SETUP_WIZARD_SKIPPED,
TIME_UPDATED;
}

View File

@ -0,0 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
public final class GarminTimeUtils {
private GarminTimeUtils() {
}
public static int unixTimeToGarminTimestamp(int unixTime) {
return unixTime - VivomoveConstants.GARMIN_TIME_EPOCH;
}
public static int javaMillisToGarminTimestamp(long millis) {
return (int) (millis / 1000) - VivomoveConstants.GARMIN_TIME_EPOCH;
}
public static long garminTimestampToJavaMillis(int timestamp) {
return (timestamp + VivomoveConstants.GARMIN_TIME_EPOCH) * 1000L;
}
public static int garminTimestampToUnixTime(int timestamp) {
return timestamp + VivomoveConstants.GARMIN_TIME_EPOCH;
}
}

View File

@ -0,0 +1,175 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
/**
* Parser of GFDI messages embedded in COBS packets.
* <p>
* COBS ensures there are no embedded NUL bytes inside the packet data, and wraps the message into NUL framing bytes.
*/
// Notes: not really optimized; does a lot of (re)allocation, might use more static buffers, I guess And code cleanup as well.
public class GfdiPacketParser {
private static final Logger LOG = LoggerFactory.getLogger(GfdiPacketParser.class);
private static final long BUFFER_TIMEOUT = 1500L;
private static final byte[] EMPTY_BUFFER = new byte[0];
private static final byte[] BUFFER_FRAMING = new byte[1];
private byte[] buffer = EMPTY_BUFFER;
private byte[] packet;
private byte[] packetBuffer;
private int bufferPos;
private long lastUpdate;
private boolean insidePacket;
public void reset() {
buffer = EMPTY_BUFFER;
bufferPos = 0;
insidePacket = false;
packet = null;
packetBuffer = EMPTY_BUFFER;
}
public void receivedBytes(byte[] bytes) {
final long now = System.currentTimeMillis();
if ((now - lastUpdate) > BUFFER_TIMEOUT) {
reset();
}
lastUpdate = now;
final int bufferSize = buffer.length;
buffer = Arrays.copyOf(buffer, bufferSize + bytes.length);
System.arraycopy(bytes, 0, buffer, bufferSize, bytes.length);
parseBuffer();
}
public byte[] retrievePacket() {
final byte[] resultPacket = packet;
packet = null;
parseBuffer();
return resultPacket;
}
private void parseBuffer() {
if (packet != null) {
// packet is waiting, unable to parse more
return;
}
if (bufferPos >= buffer.length) {
// nothing to parse
return;
}
boolean startOfPacket = !insidePacket;
if (startOfPacket) {
byte b;
while (bufferPos < buffer.length && (b = buffer[bufferPos++]) != 0) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected non-zero byte while looking for framing: {}", Integer.toHexString(b));
}
}
if (bufferPos >= buffer.length) {
// nothing to parse
return;
}
insidePacket = true;
}
boolean endedWithFullChunk = false;
while (bufferPos < buffer.length) {
int chunkSize = -1;
int chunkStart = bufferPos;
int pos = bufferPos;
while (pos < buffer.length && ((chunkSize = (buffer[pos++] & 0xFF)) == 0) && startOfPacket) {
// skip repeating framing bytes (?)
bufferPos = pos;
chunkStart = pos;
}
if (startOfPacket && pos >= buffer.length) {
// incomplete framing, needs to wait for more data and try again
buffer = BUFFER_FRAMING;
bufferPos = 0;
insidePacket = false;
return;
}
assert chunkSize >= 0;
if (chunkSize == 0) {
// end of packet
// drop the last zero
if (endedWithFullChunk) {
// except when it was explicitly added (TODO: ugly, is it correct?)
packet = packetBuffer;
} else {
packet = Arrays.copyOf(packetBuffer, packetBuffer.length - 1);
}
packetBuffer = EMPTY_BUFFER;
insidePacket = false;
if (bufferPos == buffer.length - 1) {
buffer = EMPTY_BUFFER;
bufferPos = 0;
} else {
// TODO: Realloc buffer down
++bufferPos;
}
return;
}
if (chunkStart + chunkSize > buffer.length) {
// incomplete chunk, needs to wait for more data
return;
}
// completed chunk
final int packetPos = packetBuffer.length;
final int realChunkSize = chunkSize < 255 ? chunkSize : chunkSize - 1;
packetBuffer = Arrays.copyOf(packetBuffer, packetPos + realChunkSize);
System.arraycopy(buffer, chunkStart + 1, packetBuffer, packetPos, chunkSize - 1);
bufferPos = chunkStart + chunkSize;
endedWithFullChunk = chunkSize == 255;
startOfPacket = false;
}
}
public static byte[] wrapMessageToPacket(byte[] message) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(message.length + 2 + (message.length + 253) / 254)) {
outputStream.write(0);
int chunkStart = 0;
for (int i = 0; i < message.length; ++i) {
if (message[i] == 0) {
chunkStart = appendChunk(message, outputStream, chunkStart, i);
}
}
if (chunkStart <= message.length) {
appendChunk(message, outputStream, chunkStart, message.length);
}
outputStream.write(0);
return outputStream.toByteArray();
} catch (IOException e) {
LOG.error("Error writing to memory buffer", e);
throw new RuntimeException(e);
}
}
private static int appendChunk(byte[] message, ByteArrayOutputStream outputStream, int chunkStart, int messagePos) {
int chunkLength = messagePos - chunkStart;
while (true) {
if (chunkLength >= 255) {
// write 255-byte chunk
outputStream.write(255);
outputStream.write(message, chunkStart, 254);
chunkLength -= 254;
chunkStart += 254;
} else {
// write chunk from chunkStart to here
outputStream.write(chunkLength + 1);
outputStream.write(message, chunkStart, chunkLength);
chunkStart = messagePos + 1;
break;
}
}
return chunkStart;
}
}

View File

@ -0,0 +1,166 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveHrSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.entities.VivomoveHrActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.UUID;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils.readByte;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils.readInt;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils.readShort;
/* default */ class RealTimeActivityHandler {
private static final Logger LOG = LoggerFactory.getLogger(RealTimeActivityHandler.class);
private final VivomoveHrSupport owner;
private final VivomoveHrActivitySample lastSample = new VivomoveHrActivitySample();
/* default */ RealTimeActivityHandler(VivomoveHrSupport owner) {
this.owner = owner;
}
public boolean tryHandleChangedCharacteristic(UUID characteristicUUID, byte[] data) {
if (VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_HEART_RATE.equals(characteristicUUID)) {
processRealtimeHeartRate(data);
return true;
}
if (VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_STEPS.equals(characteristicUUID)) {
processRealtimeSteps(data);
return true;
}
if (VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_CALORIES.equals(characteristicUUID)) {
processRealtimeCalories(data);
return true;
}
if (VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_STAIRS.equals(characteristicUUID)) {
processRealtimeStairs(data);
return true;
}
if (VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_INTENSITY.equals(characteristicUUID)) {
processRealtimeIntensityMinutes(data);
return true;
}
if (VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_HEART_RATE_VARIATION.equals(characteristicUUID)) {
handleRealtimeHeartbeat(data);
return true;
}
return false;
}
private void processRealtimeHeartRate(byte[] data) {
int unknown1 = readByte(data, 0);
int heartRate = readByte(data, 1);
int unknown2 = readByte(data, 2);
int unknown3 = readShort(data, 3);
lastSample.setHeartRate(heartRate);
processSample();
LOG.debug("Realtime HR {} ({}, {}, {})", heartRate, unknown1, unknown2, unknown3);
}
private void processRealtimeSteps(byte[] data) {
int steps = readInt(data, 0);
int goal = readInt(data, 4);
lastSample.setSteps(steps);
processSample();
LOG.debug("Realtime steps: {} steps (goal: {})", steps, goal);
}
private void processRealtimeCalories(byte[] data) {
int calories = readInt(data, 0);
int unknown = readInt(data, 4);
lastSample.setCaloriesBurnt(calories);
processSample();
LOG.debug("Realtime calories: {} cal burned (unknown: {})", calories, unknown);
}
private void processRealtimeStairs(byte[] data) {
int floorsClimbed = readShort(data, 0);
int unknown = readShort(data, 2);
int floorGoal = readShort(data, 4);
lastSample.setFloorsClimbed(floorsClimbed);
processSample();
LOG.debug("Realtime stairs: {} floors climbed (goal: {}, unknown: {})", floorsClimbed, floorGoal, unknown);
}
private void processRealtimeIntensityMinutes(byte[] data) {
int weeklyLimit = readInt(data, 10);
LOG.debug("Realtime intensity recorded; weekly limit: {}", weeklyLimit);
}
private void handleRealtimeHeartbeat(byte[] data) {
int interval = readShort(data, 0);
int timer = readInt(data, 2);
float heartRate = (60.0f * 1024.0f) / interval;
LOG.debug("Realtime heartbeat frequency {} at {}", heartRate, timer);
}
private void processSample() {
if (lastSample.getCaloriesBurnt() == null || lastSample.getFloorsClimbed() == null || lastSample.getHeartRate() == 0 || lastSample.getSteps() == 0) {
LOG.debug("Skipping incomplete sample");
return;
}
try (final DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final GBDevice gbDevice = owner.getDevice();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
final int ts = (int) (System.currentTimeMillis() / 1000);
final VivomoveHrSampleProvider provider = new VivomoveHrSampleProvider(gbDevice, session);
final VivomoveHrActivitySample sample = createActivitySample(device, user, ts, provider);
sample.setCaloriesBurnt(lastSample.getCaloriesBurnt());
sample.setFloorsClimbed(lastSample.getFloorsClimbed());
sample.setHeartRate(lastSample.getHeartRate());
sample.setSteps(lastSample.getSteps());
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
sample.setRawKind(ActivityKind.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that?
LOG.debug("Publishing sample");
provider.addGBActivitySample(sample);
} catch (Exception e) {
LOG.error("Error saving real-time activity data", e);
}
final Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, lastSample);
LocalBroadcastManager.getInstance(owner.getContext()).sendBroadcast(intent);
}
public VivomoveHrActivitySample createActivitySample(Device device, User user, int timestampInSeconds, VivomoveHrSampleProvider provider) {
final VivomoveHrActivitySample sample = new VivomoveHrActivitySample();
sample.setDevice(device);
sample.setUser(user);
sample.setTimestamp(timestampInSeconds);
sample.setProvider(provider);
return sample;
}
}

View File

@ -0,0 +1,91 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
import android.bluetooth.BluetoothGattCharacteristic;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
public class VivomoveHrCommunicator {
private static final Logger LOG = LoggerFactory.getLogger(VivomoveHrCommunicator.class);
private final AbstractBTLEDeviceSupport deviceSupport;
private BluetoothGattCharacteristic characteristicMessageSender;
private BluetoothGattCharacteristic characteristicMessageReceiver;
private BluetoothGattCharacteristic characteristicHeartRate;
private BluetoothGattCharacteristic characteristicSteps;
private BluetoothGattCharacteristic characteristicCalories;
private BluetoothGattCharacteristic characteristicStairs;
private BluetoothGattCharacteristic characteristicHrVariation;
private BluetoothGattCharacteristic char2_9;
public VivomoveHrCommunicator(AbstractBTLEDeviceSupport deviceSupport) {
this.deviceSupport = deviceSupport;
this.characteristicMessageSender = deviceSupport.getCharacteristic(VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_GFDI_SEND);
this.characteristicMessageReceiver = deviceSupport.getCharacteristic(VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_GFDI_RECEIVE);
this.characteristicHeartRate = deviceSupport.getCharacteristic(VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_HEART_RATE);
this.characteristicSteps = deviceSupport.getCharacteristic(VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_STEPS);
this.characteristicCalories = deviceSupport.getCharacteristic(VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_CALORIES);
this.characteristicStairs = deviceSupport.getCharacteristic(VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_STAIRS);
this.characteristicHrVariation = deviceSupport.getCharacteristic(VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_HEART_RATE_VARIATION);
this.char2_9 = deviceSupport.getCharacteristic(VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_2_9);
}
public void start(TransactionBuilder builder) {
builder.notify(characteristicMessageReceiver, true);
// builder.notify(characteristicHeartRate, true);
// builder.notify(characteristicSteps, true);
// builder.notify(characteristicCalories, true);
// builder.notify(characteristicStairs, true);
//builder.notify(char2_7, true);
// builder.notify(char2_9, true);
}
public void sendMessage(byte[] messageBytes) {
try {
final TransactionBuilder builder = deviceSupport.performInitialized("sendMessage()");
sendMessage(builder, messageBytes);
builder.queue(deviceSupport.getQueue());
} catch (IOException e) {
LOG.error("Unable to send a message", e);
}
}
private void sendMessage(TransactionBuilder builder, byte[] messageBytes) {
final byte[] packet = GfdiPacketParser.wrapMessageToPacket(messageBytes);
int remainingBytes = packet.length;
if (remainingBytes > VivomoveConstants.MAX_WRITE_SIZE) {
int position = 0;
while (remainingBytes > 0) {
final byte[] fragment = Arrays.copyOfRange(packet, position, position + Math.min(remainingBytes, VivomoveConstants.MAX_WRITE_SIZE));
builder.write(characteristicMessageSender, fragment);
position += fragment.length;
remainingBytes -= fragment.length;
}
} else {
builder.write(characteristicMessageSender, packet);
}
}
public void enableRealtimeSteps(boolean enable) {
try {
deviceSupport.performInitialized((enable ? "Enable" : "Disable") + " realtime steps").notify(characteristicSteps, enable).queue(deviceSupport.getQueue());
} catch (IOException e) {
LOG.error("Unable to change realtime steps notification to: " + enable, e);
}
}
public void enableRealtimeHeartRate(boolean enable) {
try {
deviceSupport.performInitialized((enable ? "Enable" : "Disable") + " realtime heartrate").notify(characteristicHeartRate, enable).queue(deviceSupport.getQueue());
} catch (IOException ex) {
LOG.error("Unable to change realtime steps notification to: " + enable, ex);
}
}
}

View File

@ -0,0 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ams;
public enum AmsEntity {
PLAYER,
QUEUE,
TRACK
}

View File

@ -0,0 +1,42 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ams;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageWriter;
import java.nio.charset.StandardCharsets;
public class AmsEntityAttribute {
public static final int PLAYER_ATTRIBUTE_NAME = 0;
public static final int PLAYER_ATTRIBUTE_PLAYBACK_INFO = 1;
public static final int PLAYER_ATTRIBUTE_VOLUME = 2;
public static final int QUEUE_ATTRIBUTE_INDEX = 0;
public static final int QUEUE_ATTRIBUTE_COUNT = 1;
public static final int QUEUE_ATTRIBUTE_SHUFFLE_MODE = 2;
public static final int QUEUE_ATTRIBUTE_REPEAT_MODE = 3;
public static final int TRACK_ATTRIBUTE_ARTIST = 0;
public static final int TRACK_ATTRIBUTE_ALBUM = 1;
public static final int TRACK_ATTRIBUTE_TITLE = 2;
public static final int TRACK_ATTRIBUTE_DURATION = 3;
public final AmsEntity entity;
public final int attributeID;
public final int updateFlags;
public final byte[] value;
public AmsEntityAttribute(AmsEntity entity, int attributeID, int updateFlags, String value) {
this.entity = entity;
this.attributeID = attributeID;
this.updateFlags = updateFlags;
this.value = value.getBytes(StandardCharsets.UTF_8);
if (this.value.length > 255) throw new IllegalArgumentException("Too long value");
}
public void writeToMessage(MessageWriter writer) {
writer.writeByte(entity.ordinal());
writer.writeByte(attributeID);
writer.writeByte(updateFlags);
writer.writeByte(value.length);
writer.writeBytes(value);
}
}

View File

@ -0,0 +1,6 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
public enum AncsAction {
POSITIVE,
NEGATIVE
}

View File

@ -0,0 +1,32 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
import android.util.SparseArray;
public enum AncsAndroidAction {
REPLY_TEXT_MESSAGE(94),
REPLY_INCOMING_CALL(95),
ACCEPT_INCOMING_CALL(96),
REJECT_INCOMING_CALL(97),
DISMISS_NOTIFICATION(98),
BLOCK_APPLICATION(99);
private static final SparseArray<AncsAndroidAction> valueByCode;
public final int code;
AncsAndroidAction(int code) {
this.code = code;
}
static {
final AncsAndroidAction[] values = values();
valueByCode = new SparseArray<>(values.length);
for (AncsAndroidAction value : values) {
valueByCode.append(value.code, value);
}
}
public static AncsAndroidAction getByCode(int code) {
return valueByCode.get(code);
}
}

View File

@ -0,0 +1,5 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
public enum AncsAppAttribute {
DISPLAY_NAME
}

View File

@ -0,0 +1,49 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
import android.util.SparseArray;
public enum AncsAttribute {
APP_IDENTIFIER(0),
TITLE(1, true),
SUBTITLE(2, true),
MESSAGE(3, true),
MESSAGE_SIZE(4),
DATE(5),
POSITIVE_ACTION_LABEL(6),
NEGATIVE_ACTION_LABEL(7),
// Garmin extensions
PHONE_NUMBER(126, true),
ACTIONS(127, false, true);
private static final SparseArray<AncsAttribute> valueByCode;
public final int code;
public final boolean hasLengthParam;
public final boolean hasAdditionalParams;
AncsAttribute(int code) {
this(code, false, false);
}
AncsAttribute(int code, boolean hasLengthParam) {
this(code, hasLengthParam, false);
}
AncsAttribute(int code, boolean hasLengthParam, boolean hasAdditionalParams) {
this.code = code;
this.hasLengthParam = hasLengthParam;
this.hasAdditionalParams = hasAdditionalParams;
}
static {
final AncsAttribute[] values = values();
valueByCode = new SparseArray<>(values.length);
for (AncsAttribute value : values) {
valueByCode.append(value.code, value);
}
}
public static AncsAttribute getByCode(int code) {
return valueByCode.get(code);
}
}

View File

@ -0,0 +1,11 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
public class AncsAttributeRequest {
public final AncsAttribute attribute;
public final int maxLength;
public AncsAttributeRequest(AncsAttribute attribute, int maxLength) {
this.attribute = attribute;
this.maxLength = maxLength;
}
}

View File

@ -0,0 +1,17 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
public enum AncsCategory {
OTHER,
INCOMING_CALL,
MISSED_CALL,
VOICEMAIL,
SOCIAL,
SCHEDULE,
EMAIL,
NEWS,
HEALTH_AND_FITNESS,
BUSINESS_AND_FINANCE,
LOCATION,
ENTERTAINMENT,
SMS
}

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
import android.util.SparseArray;
public enum AncsCommand {
GET_NOTIFICATION_ATTRIBUTES(0),
GET_APP_ATTRIBUTES(1),
PERFORM_NOTIFICATION_ACTION(2),
// Garmin extensions
PERFORM_ANDROID_ACTION(128);
private static final SparseArray<AncsCommand> valueByCode;
public final int code;
AncsCommand(int code) {
this.code = code;
}
static {
final AncsCommand[] values = values();
valueByCode = new SparseArray<>(values.length);
for (AncsCommand value : values) {
valueByCode.append(value.code, value);
}
}
public static AncsCommand getByCode(int code) {
return valueByCode.get(code);
}
}

View File

@ -0,0 +1,121 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageReader;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public abstract class AncsControlCommand {
private static final Logger LOG = LoggerFactory.getLogger(AncsControlCommand.class);
private static final AncsAppAttribute[] APP_ATTRIBUTE_VALUES = AncsAppAttribute.values();
private static final AncsAction[] ACTION_VALUES = AncsAction.values();
public final AncsCommand command;
protected AncsControlCommand(AncsCommand command) {
this.command = command;
}
public static AncsControlCommand parseCommand(byte[] buffer, int offset, int size) {
final int commandID = BinaryUtils.readByte(buffer, offset);
final AncsCommand command = AncsCommand.getByCode(commandID);
if (command == null) {
LOG.error("Unknown ANCS command {}", commandID);
return null;
}
switch (command) {
case GET_NOTIFICATION_ATTRIBUTES:
return createGetNotificationAttributesCommand(buffer, offset + 1, size - 1);
case GET_APP_ATTRIBUTES:
return createGetAppAttributesCommand(buffer, offset + 1, size - 1);
case PERFORM_NOTIFICATION_ACTION:
return createPerformNotificationAction(buffer, offset + 1, size - 1);
case PERFORM_ANDROID_ACTION:
return createPerformAndroidAction(buffer, offset + 1, size - 1);
default:
LOG.error("Unknown ANCS command {}", command);
return null;
}
}
private static AncsPerformAndroidAction createPerformAndroidAction(byte[] buffer, int offset, int size) {
final int notificationUID = BinaryUtils.readInt(buffer, offset);
final int actionID = BinaryUtils.readByte(buffer, offset + 4);
final AncsAndroidAction action = AncsAndroidAction.getByCode(actionID);
if (action == null) {
LOG.error("Unknown ANCS Android action {}", actionID);
return null;
}
int zero = ArrayUtils.indexOf((byte) 0, buffer, offset + 6, size - offset - 6);
if (zero < 0) zero = size;
final String text = new String(buffer, offset + 6, zero - offset - 6);
return new AncsPerformAndroidAction(notificationUID, action, text);
}
private static AncsPerformNotificationAction createPerformNotificationAction(byte[] buffer, int offset, int size) {
final MessageReader reader = new MessageReader(buffer, offset);
final int notificationUID = reader.readInt();
final int actionID = reader.readByte();
if (actionID < 0 || actionID >= ACTION_VALUES.length) {
LOG.error("Unknown ANCS action {}", actionID);
return null;
}
return new AncsPerformNotificationAction(notificationUID, ACTION_VALUES[actionID]);
}
private static AncsGetAppAttributesCommand createGetAppAttributesCommand(byte[] buffer, int offset, int size) {
int zero = ArrayUtils.indexOf((byte) 0, buffer, offset, size - offset);
if (zero < 0) zero = size;
final String appIdentifier = new String(buffer, offset, zero - offset, StandardCharsets.UTF_8);
final int attributeCount = size - (zero - offset);
final List<AncsAppAttribute> requestedAttributes = new ArrayList<>(attributeCount);
for (int i = 0; i < attributeCount; ++i) {
final int attributeID = BinaryUtils.readByte(buffer, zero + 1 + i);
if (attributeID < 0 || attributeID >= APP_ATTRIBUTE_VALUES.length) {
LOG.error("Unknown ANCS app attribute {}", attributeID);
return null;
}
final AncsAppAttribute attribute = APP_ATTRIBUTE_VALUES[attributeID];
requestedAttributes.add(attribute);
}
return new AncsGetAppAttributesCommand(appIdentifier, requestedAttributes);
}
private static AncsGetNotificationAttributeCommand createGetNotificationAttributesCommand(byte[] buffer, int offset, int size) {
final MessageReader reader = new MessageReader(buffer, offset);
final int notificationUID = reader.readInt();
int pos = 4;
final List<AncsAttributeRequest> attributes = new ArrayList<>(size);
while (pos < size) {
final int attributeID = reader.readByte();
++pos;
final AncsAttribute attribute = AncsAttribute.getByCode(attributeID);
if (attribute == null) {
LOG.error("Unknown ANCS attribute {}", attributeID);
return null;
}
final int maxLength;
if (attribute.hasLengthParam) {
maxLength = reader.readShort();
pos += 2;
} else if (attribute.hasAdditionalParams) {
maxLength = reader.readByte();
// TODO: What is this??
reader.readByte();
reader.readByte();
pos += 3;
} else {
maxLength = 0;
}
attributes.add(new AncsAttributeRequest(attribute, maxLength));
}
return new AncsGetNotificationAttributeCommand(notificationUID, attributes);
}
}

View File

@ -0,0 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
public enum AncsEvent {
NOTIFICATION_ADDED,
NOTIFICATION_MODIFIED,
NOTIFICATION_REMOVED
}

View File

@ -0,0 +1,9 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
public enum AncsEventFlag {
SILENT,
IMPORTANT,
PRE_EXISTING,
POSITIVE_ACTION,
NEGATIVE_ACTION
}

View File

@ -0,0 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
import java.util.List;
public class AncsGetAppAttributesCommand extends AncsControlCommand {
public final String appIdentifier;
public final List<AncsAppAttribute> requestedAttributes;
public AncsGetAppAttributesCommand(String appIdentifier, List<AncsAppAttribute> requestedAttributes) {
super(AncsCommand.GET_APP_ATTRIBUTES);
this.appIdentifier = appIdentifier;
this.requestedAttributes = requestedAttributes;
}
}

View File

@ -0,0 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
import java.util.List;
public class AncsGetNotificationAttributeCommand extends AncsControlCommand {
public final int notificationUID;
public final List<AncsAttributeRequest> attributes;
public AncsGetNotificationAttributeCommand(int notificationUID, List<AncsAttributeRequest> attributes) {
super(AncsCommand.GET_NOTIFICATION_ATTRIBUTES);
this.notificationUID = notificationUID;
this.attributes = attributes;
}
}

View File

@ -0,0 +1,23 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageWriter;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class AncsGetNotificationAttributesResponse {
public final byte[] packet;
public AncsGetNotificationAttributesResponse(int notificationUID, Map<AncsAttribute, String> attributes) {
final MessageWriter messageWriter = new MessageWriter();
messageWriter.writeByte(AncsCommand.GET_NOTIFICATION_ATTRIBUTES.code);
messageWriter.writeInt(notificationUID);
for(Map.Entry<AncsAttribute, String> attribute : attributes.entrySet()) {
messageWriter.writeByte(attribute.getKey().code);
final byte[] bytes = attribute.getValue().getBytes(StandardCharsets.UTF_8);
messageWriter.writeShort(bytes.length);
messageWriter.writeBytes(bytes);
}
this.packet = messageWriter.getBytes();
}
}

View File

@ -0,0 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
public class AncsPerformAndroidAction extends AncsControlCommand {
public final int notificationUID;
public final AncsAndroidAction action;
public final String text;
public AncsPerformAndroidAction(int notificationUID, AncsAndroidAction action, String text) {
super(AncsCommand.PERFORM_ANDROID_ACTION);
this.notificationUID = notificationUID;
this.action = action;
this.text = text;
}
}

View File

@ -0,0 +1,12 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
public class AncsPerformNotificationAction extends AncsControlCommand {
public final int notificationUID;
public final AncsAction action;
public AncsPerformNotificationAction(int notificationUID, AncsAction action) {
super(AncsCommand.PERFORM_NOTIFICATION_ACTION);
this.notificationUID = notificationUID;
this.action = action;
}
}

View File

@ -0,0 +1,97 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.VivomoveHrCommunicator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.GncsDataSourceMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.GncsDataSourceResponseMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedList;
import java.util.Queue;
public class GncsDataSourceQueue {
private static final Logger LOG = LoggerFactory.getLogger(GncsDataSourceQueue.class);
private final VivomoveHrCommunicator communicator;
private final int maxPacketSize;
private final Queue<byte[]> queue = new LinkedList<>();
private byte[] currentPacket;
private int currentDataOffset;
private int lastSentSize;
public GncsDataSourceQueue(VivomoveHrCommunicator communicator, int maxPacketSize) {
this.communicator = communicator;
this.maxPacketSize = maxPacketSize;
}
public void addToQueue(byte[] packet) {
queue.add(packet);
checkStartUpload();
}
public void responseReceived(GncsDataSourceResponseMessage responseMessage) {
if (currentPacket == null) {
LOG.error("Unexpected GNCS data source response, no current packet");
return;
}
switch (responseMessage.response) {
case GncsDataSourceResponseMessage.RESPONSE_TRANSFER_SUCCESSFUL:
LOG.debug("Confirmed {}B@{} GNCS transfer", lastSentSize, currentDataOffset);
currentDataOffset += lastSentSize;
if (currentDataOffset >= currentPacket.length) {
LOG.debug("ANCS packet transfer done");
currentPacket = null;
checkStartUpload();
} else {
sendNextMessage();
}
break;
case GncsDataSourceResponseMessage.RESPONSE_RESEND_LAST_DATA_PACKET:
LOG.info("Received RESEND_LAST_DATA_PACKET GNCS response");
sendNextMessage();
break;
case GncsDataSourceResponseMessage.RESPONSE_ABORT_REQUEST:
LOG.info("Received RESPONSE_ABORT_REQUEST GNCS response");
currentPacket = null;
checkStartUpload();
break;
case GncsDataSourceResponseMessage.RESPONSE_ERROR_CRC_MISMATCH:
case GncsDataSourceResponseMessage.RESPONSE_ERROR_DATA_OFFSET_MISMATCH:
default:
LOG.error("Received {} GNCS response", responseMessage.response);
currentPacket = null;
checkStartUpload();
break;
}
}
private void checkStartUpload() {
if (currentPacket != null) {
LOG.debug("Another upload is still running");
return;
}
if (queue.isEmpty()) {
LOG.debug("Nothing in queue");
return;
}
startNextUpload();
}
private void startNextUpload() {
currentPacket = queue.remove();
currentDataOffset = 0;
LOG.debug("Sending {}B ANCS data", currentPacket.length);
sendNextMessage();
}
private void sendNextMessage() {
final int remainingSize = currentPacket.length - currentDataOffset;
final int availableSize = Math.min(remainingSize, maxPacketSize);
communicator.sendMessage(new GncsDataSourceMessage(currentPacket, currentDataOffset, Math.min(remainingSize, maxPacketSize)).packet);
lastSentSize = availableSize;
}
}

View File

@ -0,0 +1,38 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.downloads;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class DirectoryData {
public final List<DirectoryEntry> entries;
public DirectoryData(List<DirectoryEntry> entries) {
this.entries = entries;
}
public static DirectoryData parse(byte[] bytes) {
int size = bytes.length;
if ((size % 16) != 0) throw new IllegalArgumentException("Invalid directory data length");
int count = (size - 16) / 16;
final MessageReader reader = new MessageReader(bytes, 16);
final List<DirectoryEntry> entries = new ArrayList<>(count);
for (int i = 0; i < count; ++i) {
final int fileIndex = reader.readShort();
final int fileDataType = reader.readByte();
final int fileSubType = reader.readByte();
final int fileNumber = reader.readShort();
final int specificFlags = reader.readByte();
final int fileFlags = reader.readByte();
final int fileSize = reader.readInt();
final Date fileDate = new Date(GarminTimeUtils.garminTimestampToJavaMillis(reader.readInt()));
entries.add(new DirectoryEntry(fileIndex, fileDataType, fileSubType, fileNumber, specificFlags, fileFlags, fileSize, fileDate));
}
return new DirectoryData(entries);
}
}

View File

@ -0,0 +1,25 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.downloads;
import java.util.Date;
public class DirectoryEntry {
public final int fileIndex;
public final int fileDataType;
public final int fileSubType;
public final int fileNumber;
public final int specificFlags;
public final int fileFlags;
public final int fileSize;
public final Date fileDate;
public DirectoryEntry(int fileIndex, int fileDataType, int fileSubType, int fileNumber, int specificFlags, int fileFlags, int fileSize, Date fileDate) {
this.fileIndex = fileIndex;
this.fileDataType = fileDataType;
this.fileSubType = fileSubType;
this.fileNumber = fileNumber;
this.specificFlags = specificFlags;
this.fileFlags = fileFlags;
this.fileSize = fileSize;
this.fileDate = fileDate;
}
}

View File

@ -0,0 +1,9 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.downloads;
public interface FileDownloadListener {
void onDirectoryDownloaded(DirectoryData directoryData);
void onFileDownloadComplete(int fileIndex, byte[] data);
void onFileDownloadError(int fileIndex);
void onDownloadProgress(long remainingBytes);
void onAllDownloadsCompleted();
}

View File

@ -0,0 +1,172 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.downloads;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.VivomoveHrCommunicator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.DownloadRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.DownloadRequestResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.FileTransferDataMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.FileTransferDataResponseMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
public class FileDownloadQueue {
private static final Logger LOG = LoggerFactory.getLogger(FileDownloadQueue.class);
private final VivomoveHrCommunicator communicator;
private final FileDownloadListener listener;
private final Queue<QueueItem> queue = new LinkedList<>();
private final Set<Integer> queuedFileIndices = new HashSet<>();
private QueueItem currentlyDownloadingItem;
private int currentCrc;
private long totalRemainingBytes;
public FileDownloadQueue(VivomoveHrCommunicator communicator, FileDownloadListener listener) {
this.communicator = communicator;
this.listener = listener;
}
public void addToDownloadQueue(int fileIndex, int dataSize) {
if (queuedFileIndices.contains(fileIndex)) {
LOG.debug("Ignoring download request of {}, already in queue", fileIndex);
return;
}
queue.add(new QueueItem(fileIndex, dataSize));
queuedFileIndices.add(fileIndex);
totalRemainingBytes += dataSize;
checkRequestNextDownload();
}
public void cancelAllDownloads() {
queue.clear();
currentlyDownloadingItem = null;
communicator.sendMessage(new FileTransferDataResponseMessage(VivomoveConstants.STATUS_ACK, FileTransferDataResponseMessage.RESPONSE_ABORT_DOWNLOAD_REQUEST, 0).packet);
}
private boolean checkRequestNextDownload() {
if (currentlyDownloadingItem != null) {
LOG.debug("Another download is pending");
return false;
}
if (queue.isEmpty()) {
LOG.debug("No download in queue");
return true;
}
requestNextDownload();
return false;
}
private void requestNextDownload() {
currentlyDownloadingItem = queue.remove();
currentCrc = 0;
final int fileIndex = currentlyDownloadingItem.fileIndex;
LOG.info("Requesting download of {} ({} B)", fileIndex, currentlyDownloadingItem.dataSize);
queuedFileIndices.remove(fileIndex);
communicator.sendMessage(new DownloadRequestMessage(fileIndex, 0, DownloadRequestMessage.REQUEST_NEW_TRANSFER, 0, 0).packet);
}
public void onDownloadRequestResponse(DownloadRequestResponseMessage responseMessage) {
if (currentlyDownloadingItem == null) {
LOG.error("Download request response arrived, but nothing is being downloaded");
return;
}
if (responseMessage.status == VivomoveConstants.STATUS_ACK && responseMessage.response == DownloadRequestResponseMessage.RESPONSE_DOWNLOAD_REQUEST_OKAY) {
LOG.info("Received response for download request of {}: {}/{}, {}B", currentlyDownloadingItem.fileIndex, responseMessage.status, responseMessage.response, responseMessage.fileSize);
totalRemainingBytes += responseMessage.fileSize - currentlyDownloadingItem.dataSize;
currentlyDownloadingItem.setDataSize(responseMessage.fileSize);
} else {
LOG.error("Received error response for download request of {}: {}/{}", currentlyDownloadingItem.fileIndex, responseMessage.status, responseMessage.response);
listener.onFileDownloadError(currentlyDownloadingItem.fileIndex);
totalRemainingBytes -= currentlyDownloadingItem.dataSize;
currentlyDownloadingItem = null;
checkRequestNextDownload();
}
}
public void onFileTransferData(FileTransferDataMessage dataMessage) {
final QueueItem currentlyDownloadingItem = this.currentlyDownloadingItem;
if (currentlyDownloadingItem == null) {
LOG.error("Download request response arrived, but nothing is being downloaded");
communicator.sendMessage(new FileTransferDataResponseMessage(VivomoveConstants.STATUS_ACK, FileTransferDataResponseMessage.RESPONSE_ABORT_DOWNLOAD_REQUEST, 0).packet);
return;
}
if (dataMessage.dataOffset < currentlyDownloadingItem.dataOffset) {
LOG.warn("Ignoring repeated transfer at offset {} of #{}", dataMessage.dataOffset, currentlyDownloadingItem.fileIndex);
communicator.sendMessage(new FileTransferDataResponseMessage(VivomoveConstants.STATUS_ACK, FileTransferDataResponseMessage.RESPONSE_ERROR_DATA_OFFSET_MISMATCH, currentlyDownloadingItem.dataOffset).packet);
return;
}
if (dataMessage.dataOffset > currentlyDownloadingItem.dataOffset) {
LOG.warn("Missing data at offset {} when received data at offset {} of #{}", currentlyDownloadingItem.dataOffset, dataMessage.dataOffset, currentlyDownloadingItem.fileIndex);
communicator.sendMessage(new FileTransferDataResponseMessage(VivomoveConstants.STATUS_ACK, FileTransferDataResponseMessage.RESPONSE_ERROR_DATA_OFFSET_MISMATCH, currentlyDownloadingItem.dataOffset).packet);
return;
}
final int dataCrc = ChecksumCalculator.computeCrc(currentCrc, dataMessage.data, 0, dataMessage.data.length);
if (dataCrc != dataMessage.crc) {
LOG.warn("Invalid CRC ({} vs {}) for {}B data @{} of {}", dataCrc, dataMessage.crc, dataMessage.data.length, dataMessage.dataOffset, currentlyDownloadingItem.fileIndex);
communicator.sendMessage(new FileTransferDataResponseMessage(VivomoveConstants.STATUS_ACK, FileTransferDataResponseMessage.RESPONSE_ERROR_CRC_MISMATCH, currentlyDownloadingItem.dataOffset).packet);
return;
}
currentCrc = dataCrc;
LOG.info("Received {}B@{}/{} of {}", dataMessage.data.length, dataMessage.dataOffset, currentlyDownloadingItem.dataSize, currentlyDownloadingItem.fileIndex);
currentlyDownloadingItem.appendData(dataMessage.data);
communicator.sendMessage(new FileTransferDataResponseMessage(VivomoveConstants.STATUS_ACK, FileTransferDataResponseMessage.RESPONSE_TRANSFER_SUCCESSFUL, currentlyDownloadingItem.dataOffset).packet);
totalRemainingBytes -= dataMessage.data.length;
listener.onDownloadProgress(totalRemainingBytes);
if (currentlyDownloadingItem.dataOffset >= currentlyDownloadingItem.dataSize) {
LOG.info("Transfer of file #{} complete, {}/{}B downloaded", currentlyDownloadingItem.fileIndex, currentlyDownloadingItem.dataOffset, currentlyDownloadingItem.dataSize);
this.currentlyDownloadingItem = null;
final boolean allDone = checkRequestNextDownload();
reportCompletedDownload(currentlyDownloadingItem);
if (allDone && isIdle()) listener.onAllDownloadsCompleted();
}
}
private boolean isIdle() {
return currentlyDownloadingItem == null;
}
private void reportCompletedDownload(QueueItem downloadedItem) {
if (downloadedItem.fileIndex == 0) {
final DirectoryData directoryData = DirectoryData.parse(downloadedItem.data);
listener.onDirectoryDownloaded(directoryData);
} else {
listener.onFileDownloadComplete(downloadedItem.fileIndex, downloadedItem.data);
}
}
private static class QueueItem {
public final int fileIndex;
public int dataSize;
public int dataOffset;
public byte[] data;
public QueueItem(int fileIndex, int dataSize) {
this.fileIndex = fileIndex;
this.dataSize = dataSize;
}
public void setDataSize(int dataSize) {
if (this.data != null) throw new IllegalStateException("Data size already set");
this.dataSize = dataSize;
this.data = new byte[dataSize];
}
public void appendData(byte[] data) {
System.arraycopy(data, 0, this.data, dataOffset, data.length);
dataOffset += data.length;
}
}
}

View File

@ -0,0 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
public final class FitBool {
public static final int FALSE = 0;
public static final int TRUE = 1;
public static final int INVALID = 255;
}

View File

@ -0,0 +1,54 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveHrSampleProvider;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class FitDbImporter {
private static final Logger LOG = LoggerFactory.getLogger(FitDbImporter.class);
private final GBDevice gbDevice;
private final FitImporter fitImporter;
public FitDbImporter(GBDevice gbDevice) {
this.gbDevice = gbDevice;
fitImporter = new FitImporter();
}
public void processFitFile(List<FitMessage> messages) {
try {
fitImporter.importFitData(messages);
} catch (Exception e) {
LOG.error("Error importing FIT data", e);
}
}
public void processData() {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
final VivomoveHrSampleProvider provider = new VivomoveHrSampleProvider(gbDevice, session);
fitImporter.processImportedData(sample -> {
sample.setDevice(device);
sample.setUser(user);
sample.setProvider(provider);
provider.addGBActivitySample(sample);
});
} catch (Exception e) {
LOG.error("Error importing FIT data", e);
}
}
}

View File

@ -0,0 +1,53 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import android.util.SparseArray;
public enum FitFieldBaseType {
ENUM(0, 1, 0xFF),
SINT8(1, 1, 0x7F),
UINT8(2, 1, 0xFF),
SINT16(3, 2, 0x7FFF),
UINT16(4, 2, 0xFFFF),
SINT32(5, 4, 0x7FFFFFFF),
UINT32(6, 4, 0xFFFFFFFF),
STRING(7, 1, ""),
FLOAT32(8, 4, 0xFFFFFFFF),
FLOAT64(9, 8, 0xFFFFFFFFFFFFFFFFL),
UINT8Z(10, 1, 0),
UINT16Z(11, 2, 0),
UINT32Z(12, 4, 0),
BYTE(13, 1, 0xFF),
SINT64(14, 8, 0x7FFFFFFFFFFFFFFFL),
UINT64(15, 8, 0xFFFFFFFFFFFFFFFFL),
UINT64Z(16, 8, 0);
public final int typeNumber;
public final int size;
public final int typeID;
public final Object invalidValue;
private static final SparseArray<FitFieldBaseType> typeForCode = new SparseArray<>(values().length);
private static final SparseArray<FitFieldBaseType> typeForID = new SparseArray<>(values().length);
static {
for (FitFieldBaseType value : values()) {
typeForCode.append(value.typeNumber, value);
typeForID.append(value.typeID, value);
}
}
FitFieldBaseType(int typeNumber, int size, Object invalidValue) {
this.typeNumber = typeNumber;
this.size = size;
this.invalidValue = invalidValue;
this.typeID = size > 1 ? (typeNumber | 0x80) : typeNumber;
}
public static FitFieldBaseType decodeTypeID(int typeNumber) {
final FitFieldBaseType type = typeForID.get(typeNumber);
if (type == null) {
throw new IllegalArgumentException("Unknown type " + typeNumber);
}
return type;
}
}

View File

@ -0,0 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import nodomain.freeyourgadget.gadgetbridge.entities.VivomoveHrActivitySample;
interface FitImportProcessor {
void onSample(VivomoveHrActivitySample sample);
}

View File

@ -0,0 +1,272 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import android.util.SparseIntArray;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveHrSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.VivomoveHrActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminTimeUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
public class FitImporter {
private static final int ACTIVITY_TYPE_ALL = -1;
private final SortedMap<Integer, List<FitEvent>> eventsPerTimestamp = new TreeMap<>();
public void importFitData(List<FitMessage> messages) {
boolean ohrEnabled = false;
int softwareVersion = -1;
int lastTimestamp = 0;
final SparseIntArray lastCycles = new SparseIntArray();
for (FitMessage message : messages) {
switch (message.definition.globalMessageID) {
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_EVENT:
//message.getField();
break;
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_SOFTWARE:
final Integer versionField = message.getIntegerField("version");
if (versionField != null) softwareVersion = versionField;
break;
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_MONITORING_INFO:
lastTimestamp = message.getIntegerField("timestamp");
break;
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_MONITORING:
lastTimestamp = processMonitoringMessage(message, ohrEnabled, lastTimestamp, lastCycles);
break;
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_OHR_SETTINGS:
final Boolean isOhrEnabled = message.getBooleanField("enabled");
if (isOhrEnabled != null) ohrEnabled = isOhrEnabled;
break;
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_SLEEP_LEVEL:
processSleepLevelMessage(message);
break;
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_MONITORING_HR_DATA:
processHrDataMessage(message);
break;
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_STRESS_LEVEL:
processStressLevelMessage(message);
break;
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_MAX_MET_DATA:
processMaxMetDataMessage(message);
break;
}
}
}
public void processImportedData(FitImportProcessor processor) {
for (final Map.Entry<Integer, List<FitEvent>> eventsForTimestamp : eventsPerTimestamp.entrySet()) {
final VivomoveHrActivitySample sample = new VivomoveHrActivitySample();
sample.setTimestamp(eventsForTimestamp.getKey());
sample.setRawKind(ActivitySample.NOT_MEASURED);
sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
sample.setSteps(ActivitySample.NOT_MEASURED);
sample.setHeartRate(ActivitySample.NOT_MEASURED);
sample.setFloorsClimbed(ActivitySample.NOT_MEASURED);
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
FitEvent.EventKind bestKind = FitEvent.EventKind.UNKNOWN;
float bestScore = Float.NEGATIVE_INFINITY;
for (final FitEvent event : eventsForTimestamp.getValue()) {
if (event.getHeartRate() > sample.getHeartRate()) {
sample.setHeartRate(event.getHeartRate());
}
if (event.getFloorsClimbed() > sample.getFloorsClimbed()) {
sample.setFloorsClimbed(event.getFloorsClimbed());
}
float score = 0;
if (event.getRawKind() > 0) score += 1;
if (event.getCaloriesBurnt() > 0) score += event.getCaloriesBurnt() * 10.0f;
if (event.getSteps() > 0) score += event.getSteps();
//if (event.getRawIntensity() > 0) score += 10.0f * event.getRawIntensity();
if (event.getKind().isBetterThan(bestKind) || (event.getKind() == bestKind && score > bestScore)) {
// if (bestScore > Float.NEGATIVE_INFINITY && event.getKind() != FitEvent.EventKind.NOT_WORN) {
// System.out.println(String.format(Locale.ROOT, "Replacing %s %d (%d cal, %d steps) with %s %d (%d cal, %d steps)", sample.getRawKind(), sample.getRawIntensity(), sample.getCaloriesBurnt(), sample.getSteps(), event.getRawKind(), event.getRawIntensity(), event.getCaloriesBurnt(), event.getSteps()));
// }
bestScore = score;
bestKind = event.getKind();
sample.setRawKind(event.getRawKind());
sample.setCaloriesBurnt(event.getCaloriesBurnt());
sample.setSteps(event.getSteps());
sample.setRawIntensity(event.getRawIntensity());
}
}
if (sample.getHeartRate() == ActivitySample.NOT_MEASURED && ((sample.getRawKind() & VivomoveHrSampleProvider.RAW_TYPE_KIND_SLEEP) != 0)) {
sample.setRawKind(VivomoveHrSampleProvider.RAW_NOT_WORN);
sample.setRawIntensity(0);
}
processor.onSample(sample);
}
}
private void processSleepLevelMessage(FitMessage message) {
final Integer timestampFull = message.getIntegerField("timestamp");
final Integer sleepLevel = message.getIntegerField("sleep_level");
final int timestamp = GarminTimeUtils.garminTimestampToUnixTime(timestampFull);
final int rawIntensity = (4 - sleepLevel) * 40;
final int rawKind = VivomoveHrSampleProvider.RAW_TYPE_KIND_SLEEP | sleepLevel;
addEvent(new FitEvent(timestamp, FitEvent.EventKind.SLEEP, rawKind, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, rawIntensity));
}
private int processMonitoringMessage(FitMessage message, boolean ohrEnabled, int lastTimestamp, SparseIntArray lastCycles) {
final Integer activityType = message.getIntegerField("activity_type");
final Double activeCalories = message.getNumericField("active_calories");
final Integer intensity = message.getIntegerField("current_activity_type_intensity");
final Integer cycles = message.getIntegerField("cycles");
final Double heartRateMeasured = message.getNumericField("heart_rate");
final Integer timestampFull = message.getIntegerField("timestamp");
final Integer timestamp16 = message.getIntegerField("timestamp_16");
final Double activeTime = message.getNumericField("active_time");
final int activityTypeOrAll = activityType == null ? ACTIVITY_TYPE_ALL : activityType;
final int activityTypeOrDefault = activityType == null ? 0 : activityType;
final int lastDefaultCycleCount = lastCycles.get(ACTIVITY_TYPE_ALL);
final int lastCycleCount = Math.max(lastCycles.get(activityTypeOrAll), lastDefaultCycleCount);
final Integer currentCycles = cycles == null ? null : cycles < lastCycleCount ? cycles : cycles - lastCycleCount;
if (currentCycles != null) {
lastCycles.put(activityTypeOrDefault, cycles);
final int newAllCycles = Math.max(lastDefaultCycleCount, cycles);
if (newAllCycles != lastDefaultCycleCount) {
assert newAllCycles > lastDefaultCycleCount;
lastCycles.put(ACTIVITY_TYPE_ALL, newAllCycles);
}
}
if (timestampFull != null) {
lastTimestamp = timestampFull;
} else if (timestamp16 != null) {
lastTimestamp += (timestamp16 - (lastTimestamp & 0xFFFF)) & 0xFFFF;
} else {
// TODO: timestamp_min_8
throw new IllegalArgumentException("Unsupported timestamp");
}
final int timestamp = GarminTimeUtils.garminTimestampToUnixTime(lastTimestamp);
final int rawKind, caloriesBurnt, floorsClimbed, heartRate, steps, rawIntensity;
final FitEvent.EventKind eventKind;
caloriesBurnt = activeCalories == null ? ActivitySample.NOT_MEASURED : (int) Math.round(activeCalories);
floorsClimbed = ActivitySample.NOT_MEASURED;
heartRate = ohrEnabled && heartRateMeasured != null && heartRateMeasured > 0 ? (int) Math.round(heartRateMeasured) : ActivitySample.NOT_MEASURED;
steps = currentCycles == null ? ActivitySample.NOT_MEASURED : currentCycles;
rawIntensity = intensity == null ? 0 : intensity;
rawKind = VivomoveHrSampleProvider.RAW_TYPE_KIND_ACTIVITY | activityTypeOrDefault;
eventKind = steps != ActivitySample.NOT_MEASURED || rawIntensity > 0 || activityTypeOrDefault > 0 ? FitEvent.EventKind.ACTIVITY : FitEvent.EventKind.WORN;
if (rawKind != ActivitySample.NOT_MEASURED
|| caloriesBurnt != ActivitySample.NOT_MEASURED
|| floorsClimbed != ActivitySample.NOT_MEASURED
|| heartRate != ActivitySample.NOT_MEASURED
|| steps != ActivitySample.NOT_MEASURED
|| rawIntensity != ActivitySample.NOT_MEASURED) {
addEvent(new FitEvent(timestamp, eventKind, rawKind, caloriesBurnt, floorsClimbed, heartRate, steps, rawIntensity));
} else {
addEvent(new FitEvent(timestamp, FitEvent.EventKind.NOT_WORN, VivomoveHrSampleProvider.RAW_NOT_WORN, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED));
}
return lastTimestamp;
}
private void processHrDataMessage(FitMessage message) {
}
private void processStressLevelMessage(FitMessage message) {
}
private void processMaxMetDataMessage(FitMessage message) {
}
private void addEvent(FitEvent event) {
List<FitEvent> eventsForTimestamp = eventsPerTimestamp.get(event.getTimestamp());
if (eventsForTimestamp == null) {
eventsForTimestamp = new ArrayList<>();
eventsPerTimestamp.put(event.getTimestamp(), eventsForTimestamp);
}
eventsForTimestamp.add(event);
}
private static class FitEvent {
private final int timestamp;
private final EventKind kind;
private final int rawKind;
private final int caloriesBurnt;
private final int floorsClimbed;
private final int heartRate;
private final int steps;
private final int rawIntensity;
private FitEvent(int timestamp, EventKind kind, int rawKind, int caloriesBurnt, int floorsClimbed, int heartRate, int steps, int rawIntensity) {
this.timestamp = timestamp;
this.kind = kind;
this.rawKind = rawKind;
this.caloriesBurnt = caloriesBurnt;
this.floorsClimbed = floorsClimbed;
this.heartRate = heartRate;
this.steps = steps;
this.rawIntensity = rawIntensity;
}
public int getTimestamp() {
return timestamp;
}
public EventKind getKind() {
return kind;
}
public int getRawKind() {
return rawKind;
}
public int getCaloriesBurnt() {
return caloriesBurnt;
}
public int getFloorsClimbed() {
return floorsClimbed;
}
public int getHeartRate() {
return heartRate;
}
public int getSteps() {
return steps;
}
public int getRawIntensity() {
return rawIntensity;
}
public enum EventKind {
UNKNOWN,
NOT_WORN,
WORN,
SLEEP,
ACTIVITY;
public boolean isBetterThan(EventKind other) {
return ordinal() > other.ordinal();
}
}
}
}

View File

@ -0,0 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
class FitLocalFieldDefinition {
public final FitMessageFieldDefinition globalDefinition;
public final int size;
public final FitFieldBaseType baseType;
FitLocalFieldDefinition(FitMessageFieldDefinition globalDefinition, int size, FitFieldBaseType baseType) {
this.globalDefinition = globalDefinition;
this.size = size;
this.baseType = baseType;
}
}

View File

@ -0,0 +1,13 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import java.util.List;
class FitLocalMessageDefinition {
public final FitMessageDefinition globalDefinition;
public final List<FitLocalFieldDefinition> fieldDefinitions;
FitLocalMessageDefinition(FitMessageDefinition globalDefinition, List<FitLocalFieldDefinition> fieldDefinitions) {
this.globalDefinition = globalDefinition;
this.fieldDefinitions = fieldDefinitions;
}
}

View File

@ -0,0 +1,165 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageWriter;
import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class FitMessage {
public final FitMessageDefinition definition;
private final SparseArray<Object> fieldValues = new SparseArray<>();
private final Map<String, Object> fieldValuesPerName = new HashMap<>();
public FitMessage(FitMessageDefinition definition) {
this.definition = definition;
}
public void setField(int fieldNumber, Object value) {
// TODO: Support arrays
fieldValues.append(fieldNumber, value);
final FitMessageFieldDefinition fieldDefinition = definition.getField(fieldNumber);
fieldValuesPerName.put(fieldDefinition.fieldName, value);
}
public void setField(String fieldName, Object value) {
final FitMessageFieldDefinition fieldDefinition = definition.findField(fieldName);
if (fieldDefinition == null) throw new IllegalArgumentException("Unknown field name " + fieldName);
setField(fieldDefinition.fieldNumber, value);
}
public Object getField(int fieldNumber) {
return fieldValues.get(fieldNumber);
}
public Object getField(String fieldName) {
return fieldValuesPerName.get(fieldName);
}
public String getStringField(String fieldName) {
return (String) getField(fieldName);
}
public Integer getIntegerField(String fieldName) {
return (Integer) getField(fieldName);
}
public Double getNumericField(String fieldName) {
return (Double) getField(fieldName);
}
public Boolean getBooleanField(String fieldName) {
final Integer value = (Integer) getField(fieldName);
if (value == null) return null;
int v = value;
return v == FitBool.INVALID ? null : (v != 0);
}
public boolean isBooleanFieldTrue(String fieldName) {
final Boolean value = getBooleanField(fieldName);
return value != null && value;
}
public void writeToMessage(MessageWriter writer) {
writer.writeByte(definition.localMessageID);
for (FitMessageFieldDefinition fieldDefinition : definition.fieldDefinitions) {
final Object value = fieldValues.get(fieldDefinition.fieldNumber, fieldDefinition.defaultValue);
writeFitValueToMessage(writer, value, fieldDefinition.fieldType, fieldDefinition.fieldSize);
}
}
private static void writeFitValueToMessage(MessageWriter writer, Object value, FitFieldBaseType type, int size) {
switch (type) {
case ENUM:
case SINT8:
case UINT8:
case SINT16:
case UINT16:
case SINT32:
case UINT32:
case UINT8Z:
case UINT16Z:
case UINT32Z:
case BYTE:
writeFitNumberToMessage(writer, (Integer) value, size);
break;
case SINT64:
case UINT64:
case UINT64Z:
writeFitNumberToMessage(writer, (Long) value, size);
break;
case STRING:
writeFitStringToMessage(writer, (String) value, size);
break;
// TODO: Float data types
default:
throw new IllegalArgumentException("Unable to write value of type " + type);
}
}
private static void writeFitStringToMessage(MessageWriter writer, String value, int size) {
final byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
int valueSize = Math.min(bytes.length, size - 1);
writer.writeBytes(bytes, 0, valueSize);
for (int i = valueSize; i < size; ++i) {
writer.writeByte(0);
}
}
private static void writeFitNumberToMessage(MessageWriter writer, long value, int size) {
switch (size) {
case 1:
writer.writeByte((int) value);
break;
case 2:
writer.writeShort((int) value);
break;
case 4:
writer.writeInt((int) value);
break;
case 8:
writer.writeLong(value);
break;
default:
throw new IllegalArgumentException("Unable to write number of size " + size);
}
}
@Override
@NonNull
public String toString() {
final StringBuilder result = new StringBuilder();
result.append(this.definition.messageName);
result.append(System.lineSeparator());
for (Map.Entry<String, Object> field : fieldValuesPerName.entrySet()) {
result.append('\t');
result.append(field.getKey());
result.append(": ");
result.append(valueToString(field.getValue()));
result.append(System.lineSeparator());
}
return result.toString();
}
@NonNull
private static String valueToString(Object value) {
if (value == null) return "null";
final Class<?> clazz = value.getClass();
if (clazz.isArray()) {
final StringBuilder result = new StringBuilder();
result.append('[');
final int length = Array.getLength(value);
for (int i = 0; i < length; ++i) {
if (i > 0) result.append(", ");
result.append(valueToString(Array.get(value, i)));
}
result.append(']');
return result.toString();
} else {
return String.valueOf(value);
}
}
}

View File

@ -0,0 +1,55 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import android.util.SparseArray;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageWriter;
import java.util.Arrays;
import java.util.List;
public class FitMessageDefinition {
public final String messageName;
public final int globalMessageID;
public final int localMessageID;
public final List<FitMessageFieldDefinition> fieldDefinitions;
public final SparseArray<FitMessageFieldDefinition> fieldsPerNumber;
public FitMessageDefinition(String messageName, int globalMessageID, int localMessageID, FitMessageFieldDefinition... fieldDefinitions) {
this.messageName = messageName;
this.globalMessageID = globalMessageID;
this.localMessageID = localMessageID;
this.fieldDefinitions = Arrays.asList(fieldDefinitions);
fieldsPerNumber = new SparseArray<>(fieldDefinitions.length);
for (FitMessageFieldDefinition fieldDefinition : fieldDefinitions) {
addField(fieldDefinition);
}
}
public FitMessageFieldDefinition getField(int fieldNumber) {
return fieldsPerNumber.get(fieldNumber);
}
public void writeToMessage(MessageWriter writer) {
writer.writeByte(localMessageID | 0x40);
writer.writeByte(0);
writer.writeByte(0);
writer.writeShort(globalMessageID);
writer.writeByte(fieldDefinitions.size());
for (FitMessageFieldDefinition fieldDefinition : fieldDefinitions) {
fieldDefinition.writeToMessage(writer);
}
}
public void addField(FitMessageFieldDefinition fieldDefinition) {
if (fieldsPerNumber.get(fieldDefinition.fieldNumber) != null) {
throw new IllegalArgumentException("Duplicate field number " + fieldDefinition.fieldNumber + " in message " + globalMessageID);
}
fieldsPerNumber.append(fieldDefinition.fieldNumber, fieldDefinition);
}
public FitMessageFieldDefinition findField(String fieldName) {
for (final FitMessageFieldDefinition fieldDefinition : fieldDefinitions) {
if (fieldName.equals(fieldDefinition.fieldName)) return fieldDefinition;
}
return null;
}
}

View File

@ -0,0 +1,646 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import java.util.Arrays;
import java.util.List;
public class FitMessageDefinitions {
public static final int MESSAGE_ID_CONNECTIVITY = 0;
public static final int MESSAGE_ID_WEATHER_ALERT = 5;
public static final int MESSAGE_ID_WEATHER_CONDITIONS = 6;
public static final int MESSAGE_ID_DEVICE_SETTINGS = 7;
public static final String FIT_MESSAGE_NAME_FILE_ID = "file_id";
public static final String FIT_MESSAGE_NAME_CAPABILITIES = "capabilities";
public static final String FIT_MESSAGE_NAME_DEVICE_SETTINGS = "device_settings";
public static final String FIT_MESSAGE_NAME_USER_PROFILE = "user_profile";
public static final String FIT_MESSAGE_NAME_EVENT = "event";
public static final String FIT_MESSAGE_NAME_DEVICE_INFO = "device_info";
public static final String FIT_MESSAGE_NAME_DEBUG = "debug";
public static final String FIT_MESSAGE_NAME_SOFTWARE = "software";
public static final String FIT_MESSAGE_NAME_FILE_CAPABILITIES = "file_capabilities";
public static final String FIT_MESSAGE_NAME_FILE_CREATOR = "file_creator";
public static final String FIT_MESSAGE_NAME_MONITORING = "monitoring";
public static final String FIT_MESSAGE_NAME_MONITORING_INFO = "monitoring_info";
public static final String FIT_MESSAGE_NAME_CONNECTIVITY = "connectivity";
public static final String FIT_MESSAGE_NAME_WEATHER_CONDITIONS = "weather_conditions";
public static final String FIT_MESSAGE_NAME_WEATHER_ALERT = "weather_alert";
public static final String FIT_MESSAGE_NAME_FILE_DESCRIPTION = "file_description";
public static final String FIT_MESSAGE_NAME_OHR_SETTINGS = "ohr_settings";
public static final String FIT_MESSAGE_NAME_EXD_SCREEN_CONFIGURATION = "exd_screen_configuration";
public static final String FIT_MESSAGE_NAME_EXD_DATA_FIELD_CONFIGURATION = "exd_data_field_configuration";
public static final String FIT_MESSAGE_NAME_EXD_DATA_CONCEPT_CONFIGURATION = "exd_data_concept_configuration";
public static final String FIT_MESSAGE_NAME_MONITORING_HR_DATA = "monitoring_hr_data";
public static final String FIT_MESSAGE_NAME_ALARM_SETTINGS = "alarm_settings";
public static final String FIT_MESSAGE_NAME_STRESS_LEVEL = "stress_level";
public static final String FIT_MESSAGE_NAME_MANUAL_STRESS_LEVEL = "manual_stress_level";
public static final String FIT_MESSAGE_NAME_MAX_MET_DATA = "max_met_data";
public static final String FIT_MESSAGE_NAME_WHR_DIAG = "whr_diag";
public static final String FIT_MESSAGE_NAME_METRICS_INFO = "metrics_info";
public static final String FIT_MESSAGE_NAME_PAGES_MAP = "pages_map";
public static final String FIT_MESSAGE_NAME_NEURAL_NETWORK_INFO = "neural_network_info";
public static final String FIT_MESSAGE_NAME_NEURAL_NETWORK_DATA = "neural_network_data";
public static final String FIT_MESSAGE_NAME_SLEEP_LEVEL = "sleep_level";
public static final String FIT_MESSAGE_NAME_END_OF_FILE = "end_of_file";
public static final int FIT_MESSAGE_NUMBER_FILE_ID = 0;
public static final int FIT_MESSAGE_NUMBER_CAPABILITIES = 1;
public static final int FIT_MESSAGE_NUMBER_DEVICE_SETTINGS = 2;
public static final int FIT_MESSAGE_NUMBER_USER_PROFILE = 3;
public static final int FIT_MESSAGE_NUMBER_EVENT = 21;
public static final int FIT_MESSAGE_NUMBER_DEVICE_INFO = 23;
public static final int FIT_MESSAGE_NUMBER_DEBUG = 24;
public static final int FIT_MESSAGE_NUMBER_SOFTWARE = 35;
public static final int FIT_MESSAGE_NUMBER_FILE_CAPABILITIES = 37;
public static final int FIT_MESSAGE_NUMBER_FILE_CREATOR = 49;
public static final int FIT_MESSAGE_NUMBER_MONITORING = 55;
public static final int FIT_MESSAGE_NUMBER_MONITORING_INFO = 103;
public static final int FIT_MESSAGE_NUMBER_CONNECTIVITY = 127;
public static final int FIT_MESSAGE_NUMBER_WEATHER_CONDITIONS = 128;
public static final int FIT_MESSAGE_NUMBER_WEATHER_ALERT = 129;
public static final int FIT_MESSAGE_NUMBER_FILE_DESCRIPTION = 138;
public static final int FIT_MESSAGE_NUMBER_OHR_SETTINGS = 188;
public static final int FIT_MESSAGE_NUMBER_EXD_SCREEN_CONFIGURATION = 200;
public static final int FIT_MESSAGE_NUMBER_EXD_DATA_FIELD_CONFIGURATION = 201;
public static final int FIT_MESSAGE_NUMBER_EXD_DATA_CONCEPT_CONFIGURATION = 202;
public static final int FIT_MESSAGE_NUMBER_MONITORING_HR_DATA = 211;
public static final int FIT_MESSAGE_NUMBER_ALARM_SETTINGS = 222;
public static final int FIT_MESSAGE_NUMBER_STRESS_LEVEL = 227;
public static final int FIT_MESSAGE_NUMBER_MANUAL_STRESS_LEVEL = 228;
public static final int FIT_MESSAGE_NUMBER_MAX_MET_DATA = 229;
public static final int FIT_MESSAGE_NUMBER_WHR_DIAG = 233;
public static final int FIT_MESSAGE_NUMBER_METRICS_INFO = 241;
public static final int FIT_MESSAGE_NUMBER_PAGES_MAP = 254;
public static final int FIT_MESSAGE_NUMBER_NEURAL_NETWORK_INFO = 273;
public static final int FIT_MESSAGE_NUMBER_NEURAL_NETWORK_DATA = 274;
public static final int FIT_MESSAGE_NUMBER_SLEEP_LEVEL = 275;
public static final int FIT_MESSAGE_NUMBER_END_OF_FILE = 276;
public static final FitMessageDefinition DEFINITION_FILE_ID = new FitMessageDefinition(FIT_MESSAGE_NAME_FILE_ID, FIT_MESSAGE_NUMBER_FILE_ID, -1,
new FitMessageFieldDefinition("type", 0, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("manufacturer", 1, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("product", 2, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("serial_number", 3, 4, FitFieldBaseType.UINT32Z, null),
new FitMessageFieldDefinition("time_created", 4, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("number", 5, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("manufacturer_partner", 6, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("product_name", 8, 20, FitFieldBaseType.STRING, null)
);
public static final FitMessageDefinition DEFINITION_CAPABILITIES = new FitMessageDefinition(FIT_MESSAGE_NAME_CAPABILITIES, FIT_MESSAGE_NUMBER_CAPABILITIES, -1,
new FitMessageFieldDefinition("languages", 0, 1, FitFieldBaseType.UINT8Z, null),
new FitMessageFieldDefinition("sports", 1, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("workouts_supported", 21, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("activity_profile_supported", 22, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("connectivity_supported", 23, 4, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("wifi_supported", 24, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("segments_supported", 25, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("audio_prompts_supported", 26, 1, FitFieldBaseType.ENUM, null)
);
public static final FitMessageDefinition DEFINITION_DEVICE_SETTINGS = new FitMessageDefinition(FIT_MESSAGE_NAME_DEVICE_SETTINGS, FIT_MESSAGE_NUMBER_DEVICE_SETTINGS, MESSAGE_ID_DEVICE_SETTINGS,
new FitMessageFieldDefinition("active_time_zone", 0, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("utc_offset", 1, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("time_offset", 2, 4, FitFieldBaseType.UINT32, 1, 0, "s", null),
new FitMessageFieldDefinition("time_daylight_savings", 3, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("time_mode", 4, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("time_zone_offset", 5, 1, FitFieldBaseType.SINT8, 1, 0, "hr", null),
new FitMessageFieldDefinition("alarm_time", 8, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("alarm_mode", 9, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("key_tones_enabled", 10, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("message_tones_enabled", 11, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("backlight_mode", 12, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("backlight_timeout", 13, 1, FitFieldBaseType.UINT8, 1, 0, "s", null),
new FitMessageFieldDefinition("backlight_brightness", 14, 1, FitFieldBaseType.UINT8, 1, 0, "%", null),
new FitMessageFieldDefinition("display_contrast", 15, 1, FitFieldBaseType.UINT8, 1, 0, "%", null),
new FitMessageFieldDefinition("computer_beacon", 16, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("computer_pairing", 17, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("fitness_equipment_pairing", 18, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("bezel_sensitivity", 19, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("gps_enabled", 21, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("weight_scale_enabled", 22, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("map_orientation", 23, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("map_show", 24, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("map_show_locations", 25, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("time_zone", 26, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("auto_shutdown", 27, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("alarm_tone", 28, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("data_storage", 29, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("map_auto_zoom", 30, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("map_guidance", 31, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("current_map_profile", 32, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("current_routing_profile", 33, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("display_mode", 34, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("first_day_of_week", 35, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("activity_tracker_enabled", 36, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("sleep_enabled", 37, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("wifi_auto_upload_enabled", 38, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("clock_time", 39, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("pages_enabled", 40, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("recovery_advisor_enabled", 41, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("auto_max_hr_enabled", 42, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("clock_profile_color_enabled", 43, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("clock_background_inverted", 44, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("auto_goal_enabled", 45, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("move_alert_enabled", 46, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("date_mode", 47, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("data_recording_interval", 48, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("data_recording_value", 49, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("vivohub_settings", 50, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("display_steps_goal_enabled", 51, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("course_navigation_enabled", 52, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("course_off_course_warnings_enabled", 53, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("segment_navigation_enabled", 54, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("display_orientation", 55, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("mounting_side", 56, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("default_page", 57, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("autosync_min_steps", 58, 2, FitFieldBaseType.UINT16, 1, 0, "steps", null),
new FitMessageFieldDefinition("autosync_min_time", 59, 2, FitFieldBaseType.UINT16, 1, 0, "minutes", null),
new FitMessageFieldDefinition("smart_sleep_window", 60, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("gesture_detection_mode", 61, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("glonass_enabled", 62, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("display_pace", 63, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("display_activity_tracker_enabled", 64, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("phone_notification_enabled", 65, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("phone_notification_tone", 66, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("phone_notification_default_filter", 67, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("phone_notification_activity_filter", 68, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("phone_notification_activity_tone", 69, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("user_notices_enabled", 70, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("lap_key_enabled", 71, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("features", 72, 1, FitFieldBaseType.UINT8Z, null),
new FitMessageFieldDefinition("features_mask", 73, 1, FitFieldBaseType.UINT8Z, null),
new FitMessageFieldDefinition("course_points_enabled", 74, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("course_segments_enabled", 75, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("map_show_track", 76, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("map_track_color", 77, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("next_dst_change", 78, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("dst_change_value", 79, 1, FitFieldBaseType.SINT8, 1, 0, "hours", null),
new FitMessageFieldDefinition("lactate_threshold_autodetect_enabled", 80, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("backlight_keys", 81, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("backlight_alerts", 82, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("backlight_gesture", 83, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("bluetooth_connection_alerts_enabled", 84, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("ftp_auto_calc_enabled", 85, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("ble_auto_upload_enabled", 86, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("sleep_do_not_disturb_enabled", 87, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("backlight_smart_notifications", 88, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("auto_sync_frequency", 89, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("auto_activity_detect", 90, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("phone_notification_filters", 91, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("alarm_days", 92, 1, FitFieldBaseType.BYTE, null),
new FitMessageFieldDefinition("auto_update_app_enabled", 93, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("number_of_screens", 94, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("smart_notification_display_orientation", 95, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("auto_lock_enabled", 96, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("grouptrack_activity_type", 97, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("wifi_enabled", 98, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("smart_notification_enabled", 99, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("beeper_enabled", 100, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("goal_notification", 101, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("product_category", 102, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("touch_sensitivity", 103, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("power_controls_items", 104, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("selected_watchface_index", 105, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("livetrack_message_notification_enabled", 106, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("alert_tones_app_only", 107, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("auto_detect_max_hr", 108, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("perf_cond_ntfctn_enabled", 109, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("new_vo2_ntfctn_enabled", 110, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("training_effect_ntfctn_enabled", 111, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("recovery_time_ntfctn_enabled", 112, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("auto_activity_start_enabled", 113, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("move_bar_enabled", 114, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("vibration_intensity", 115, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("lock_on_road", 116, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("map_detail", 117, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("screen_timeout", 119, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("display_theme", 120, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("popularity_routing_enabled", 121, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("glance_mode_layout", 122, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("user_text", 123, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("backlight_brightness_current_activity", 124, 1, FitFieldBaseType.UINT8, 1, 0, "%", null),
new FitMessageFieldDefinition("backlight_timeout_current_activity", 125, 1, FitFieldBaseType.UINT8, 1, 0, "s", null),
new FitMessageFieldDefinition("backlight_keys_current_activity", 126, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("backlight_alerts_current_activity", 127, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("backlight_gesture_current_activity", 128, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("marine_chart_mode", 129, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("spot_soundings", 130, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("light_sectors", 131, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("marine_symbol_set", 132, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("auto_update_software_enabled", 133, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("tap_interface", 134, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("auto_lock_mode", 135, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("simplified_backlight_timeout", 136, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("draw_segments", 137, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("hourly_alert", 138, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("turn_guidance_popup", 139, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("stress_alert_enabled", 140, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("spo2_mode", 141, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("low_spo2_threshold", 142, 1, FitFieldBaseType.UINT8, 1, 0, "percent", null),
new FitMessageFieldDefinition("sedentary_hr_alert_threshold", 143, 1, FitFieldBaseType.UINT8, 1, 0, "bpm", null),
new FitMessageFieldDefinition("activity_physio_true_up_enabled", 144, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("smart_notification_timeout", 145, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("sideswipe_enabled", 146, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("sideswipe_direction_inverted", 147, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("draw_contour_lines", 148, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("sedentary_hr_alert_state", 149, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("autosync_max_steps", 150, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("low_spo2_alert_enabled", 151, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("cda_auto_calc_enabled", 152, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("hydration_system_units", 153, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("hydration_containers", 154, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("hydration_alert_enabled", 155, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("hydration_alert_frequency", 156, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("hydration_containers_units", 157, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("hydration_auto_goal_enabled", 158, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("user_phone_verified", 159, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("primary_tracker_enabled", 160, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("phone_notification_default_privacy", 161, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("phone_notification_activity_privacy", 162, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("abnormal_low_hr_alert_state", 163, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("abnormal_low_hr_alert_threshold", 164, 1, FitFieldBaseType.UINT8, 1, 0, "bpm", null)
);
public static final FitMessageDefinition DEFINITION_USER_PROFILE = new FitMessageDefinition(FIT_MESSAGE_NAME_USER_PROFILE, FIT_MESSAGE_NUMBER_USER_PROFILE, -1,
new FitMessageFieldDefinition("friendly_name", 0, 16, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("gender", 1, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("age", 2, 1, FitFieldBaseType.UINT8, 1, 0, "years", null),
new FitMessageFieldDefinition("height", 3, 1, FitFieldBaseType.UINT8, 1, 0, "cm", null),
new FitMessageFieldDefinition("weight", 4, 2, FitFieldBaseType.UINT16, 10, 0, "kg", null),
new FitMessageFieldDefinition("language", 5, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("elev_setting", 6, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("weight_setting", 7, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("resting_heart_rate", 8, 1, FitFieldBaseType.UINT8, 1, 0, "bpm", null),
new FitMessageFieldDefinition("default_max_running_heart_rate", 9, 1, FitFieldBaseType.UINT8, 1, 0, "bpm", null),
new FitMessageFieldDefinition("default_max_biking_heart_rate", 10, 1, FitFieldBaseType.UINT8, 1, 0, "bpm", null),
new FitMessageFieldDefinition("default_max_heart_rate", 11, 1, FitFieldBaseType.UINT8, 1, 0, "bpm", null),
new FitMessageFieldDefinition("hr_setting", 12, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("speed_setting", 13, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("dist_setting", 14, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("power_setting", 16, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("activity_class", 17, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("position_setting", 18, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("rmr", 19, 2, FitFieldBaseType.UINT16, 1, 0, "kcal/d", null),
new FitMessageFieldDefinition("active_time", 20, 1, FitFieldBaseType.UINT8, 1, 0, "min", null),
new FitMessageFieldDefinition("temperature_setting", 21, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("local_id", 22, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("global_id", 23, 6, FitFieldBaseType.BYTE, null),
new FitMessageFieldDefinition("birth_year", 24, 1, FitFieldBaseType.UINT8, 1, 1900, "", null),
new FitMessageFieldDefinition("avg_cycle_length", 25, 2, FitFieldBaseType.UINT16, 10000, 0, "m", null),
new FitMessageFieldDefinition("pressure_setting", 26, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("handedness", 27, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("wake_time", 28, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("sleep_time", 29, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("height_setting", 30, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("user_running_step_length", 31, 2, FitFieldBaseType.UINT16, 1, 0, "mm", null),
new FitMessageFieldDefinition("user_walking_step_length", 32, 2, FitFieldBaseType.UINT16, 1, 0, "mm", null),
new FitMessageFieldDefinition("firstbeat_monthly_load", 33, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("firstbeat_recovery_time", 34, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("firstbeat_recovery_time_start", 35, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("firstbeat_max_stress_score", 36, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("firstbeat_running_lt_kmh", 37, 2, FitFieldBaseType.UINT16, 10, 0, "km/h", null),
new FitMessageFieldDefinition("firstbeat_cycling_lt_watts", 38, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("firstbeat_running_maxMET", 39, 4, FitFieldBaseType.FLOAT32, null),
new FitMessageFieldDefinition("firstbeat_cycling_maxMET", 40, 4, FitFieldBaseType.FLOAT32, null),
new FitMessageFieldDefinition("firstbeat_running_lt_timestamp", 41, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("firstbeat_cycling_lt_timestamp", 42, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("resting_hr_auto_update_enabled", 43, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("birth_day", 44, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("birth_month", 45, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("avatar", 46, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("depth_setting", 47, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("dive_count", 49, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("phone_number", 50, 20, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("keep_user_name_private", 51, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("active_minutes_calc_method", 52, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("active_minutes_moderate_zone", 53, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("active_minutes_vigorous_zone", 54, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("swim_skill_level", 55, 1, FitFieldBaseType.UINT8, null)
);
public static final FitMessageDefinition DEFINITION_EVENT = new FitMessageDefinition(FIT_MESSAGE_NAME_EVENT, FIT_MESSAGE_NUMBER_EVENT, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("event", 0, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("event_type", 1, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("data16", 2, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("data", 3, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("event_group", 4, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("device_index", 13, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("activity_type", 14, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("start_timestamp", 15, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("activity_subtype", 16, 1, FitFieldBaseType.ENUM, null)
);
public static final FitMessageDefinition DEFINITION_DEVICE_INFO = new FitMessageDefinition(FIT_MESSAGE_NAME_DEVICE_INFO, FIT_MESSAGE_NUMBER_DEVICE_INFO, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("device_index", 0, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("device_type", 1, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("manufacturer", 2, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("serial_number", 3, 4, FitFieldBaseType.UINT32Z, null),
new FitMessageFieldDefinition("product", 4, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("software_version", 5, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("hardware_version", 6, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("cum_operating_time", 7, 4, FitFieldBaseType.UINT32, 1, 0, "s", null),
new FitMessageFieldDefinition("cum_training_time", 8, 4, FitFieldBaseType.UINT32, 1, 0, "s", null),
new FitMessageFieldDefinition("reception", 9, 4, FitFieldBaseType.UINT8, 1, 0, "%", null),
new FitMessageFieldDefinition("battery_voltage", 10, 2, FitFieldBaseType.UINT16, 256, 0, "V", null),
new FitMessageFieldDefinition("battery_status", 11, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("rx_pass_count", 15, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("rx_fail_count", 16, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("software_version_string", 17, 20, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("sensor_position", 18, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("descriptor", 19, 20, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("ant_transmission_type", 20, 1, FitFieldBaseType.UINT8Z, null),
new FitMessageFieldDefinition("ant_device_number", 21, 2, FitFieldBaseType.UINT16Z, null),
new FitMessageFieldDefinition("ant_network", 22, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("source_type", 25, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("product_name", 27, 20, FitFieldBaseType.STRING, null)
);
public static final FitMessageDefinition DEFINITION_DEBUG = new FitMessageDefinition(FIT_MESSAGE_NAME_DEBUG, FIT_MESSAGE_NUMBER_DEBUG, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("id", 0, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("string", 1, 20, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("data", 2, 20, FitFieldBaseType.BYTE, null),
new FitMessageFieldDefinition("time256", 3, 20, FitFieldBaseType.UINT8, 256, 0, "s", null),
new FitMessageFieldDefinition("fractional_timestamp", 4, 2, FitFieldBaseType.UINT16, 32768.0, 0, "s", null)
);
public static final FitMessageDefinition DEFINITION_SOFTWARE = new FitMessageDefinition(FIT_MESSAGE_NAME_SOFTWARE, FIT_MESSAGE_NUMBER_SOFTWARE, -1,
new FitMessageFieldDefinition("message_index", 254, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("version", 3, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("part_number", 5, 20, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("version_string", 6, 20, FitFieldBaseType.STRING, null)
);
public static final FitMessageDefinition DEFINITION_FILE_CAPABILITIES = new FitMessageDefinition(FIT_MESSAGE_NAME_FILE_CAPABILITIES, FIT_MESSAGE_NUMBER_FILE_CAPABILITIES, -1,
new FitMessageFieldDefinition("type", 0, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("flags", 1, 1, FitFieldBaseType.UINT8Z, null),
new FitMessageFieldDefinition("directory", 2, 16, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("max_count", 3, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("max_size", 4, 4, FitFieldBaseType.UINT32, null)
);
public static final FitMessageDefinition DEFINITION_FILE_CREATOR = new FitMessageDefinition(FIT_MESSAGE_NAME_FILE_CREATOR, FIT_MESSAGE_NUMBER_FILE_CREATOR, -1,
new FitMessageFieldDefinition("software_version", 0, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("hardware_version", 1, 2, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("creator_name", 2, 2, FitFieldBaseType.STRING, null)
);
public static final FitMessageDefinition DEFINITION_MONITORING = new FitMessageDefinition(FIT_MESSAGE_NAME_MONITORING, FIT_MESSAGE_NUMBER_MONITORING, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("device_index", 0, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("calories", 1, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("distance", 2, 4, FitFieldBaseType.UINT32, 100, 0, "m", null),
// TODO: Scale depends on activity type
new FitMessageFieldDefinition("cycles", 3, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("active_time", 4, 1, FitFieldBaseType.UINT32, 1000, 0, "s", null),
new FitMessageFieldDefinition("activity_type", 5, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("activity_subtype", 6, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("activity_level", 7, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("distance_16", 8, 2, FitFieldBaseType.UINT16, 0.01, 0, "m", null),
new FitMessageFieldDefinition("cycles_16", 9, 2, FitFieldBaseType.UINT16, 0.5, 0, "cycles", null),
new FitMessageFieldDefinition("active_time_16", 10, 2, FitFieldBaseType.UINT16, 1, 0, "s", null),
new FitMessageFieldDefinition("local_timestamp", 11, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("temperature", 12, 2, FitFieldBaseType.SINT16, 100, 0, "°C", null),
new FitMessageFieldDefinition("temperature_min", 14, 2, FitFieldBaseType.SINT16, 100, 0, "°C", null),
new FitMessageFieldDefinition("temperature_max", 15, 2, FitFieldBaseType.SINT16, 100, 0, "°C", null),
// TODO: Array
new FitMessageFieldDefinition("activity_time", 16, 2, FitFieldBaseType.UINT16, 1, 0, "min", null),
new FitMessageFieldDefinition("active_calories", 19, 2, FitFieldBaseType.UINT16, 1, 0, "kcal", null),
new FitMessageFieldDefinition("current_activity_type_intensity", 24, 1, FitFieldBaseType.BYTE, null),
new FitMessageFieldDefinition("timestamp_min_8", 25, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("timestamp_16", 26, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("heart_rate", 27, 1, FitFieldBaseType.UINT8, 1, 0, "bpm", null),
new FitMessageFieldDefinition("intensity", 28, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("duration_min", 29, 2, FitFieldBaseType.UINT16, 1, 0, "min", null),
new FitMessageFieldDefinition("duration", 30, 4, FitFieldBaseType.UINT32, 1, 0, "s", null),
new FitMessageFieldDefinition("ascent", 31, 4, FitFieldBaseType.UINT32, 1000, 0, "m", null),
new FitMessageFieldDefinition("descent", 32, 4, FitFieldBaseType.UINT32, 1000, 0, "m", null),
new FitMessageFieldDefinition("moderate_activity_minutes", 33, 2, FitFieldBaseType.UINT16, 1, 0, "min", null),
new FitMessageFieldDefinition("vigorous_activity_minutes", 34, 2, FitFieldBaseType.UINT16, 1, 0, "min", null),
new FitMessageFieldDefinition("ascent_total", 35, 4, FitFieldBaseType.UINT32, 1000, 0, "m", null),
new FitMessageFieldDefinition("descent_total", 36, 4, FitFieldBaseType.UINT32, 1000, 0, "m", null),
new FitMessageFieldDefinition("moderate_activity_minutes_total", 37, 2, FitFieldBaseType.UINT16, 1, 0, "min", null),
new FitMessageFieldDefinition("vigorous_activity_minutes_total", 38, 2, FitFieldBaseType.UINT16, 1, 0, "min", null)
);
public static final FitMessageDefinition DEFINITION_MONITORING_INFO = new FitMessageDefinition(FIT_MESSAGE_NAME_MONITORING_INFO, FIT_MESSAGE_NUMBER_MONITORING_INFO, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("local_timestamp", 0, 4, FitFieldBaseType.UINT32, null),
// TODO: Arrays
new FitMessageFieldDefinition("activity_type", 1, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("cycles_to_distance", 3, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("cycles_to_calories", 4, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("resting_metabolic_rate", 5, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("cycles_goal", 7, 4, FitFieldBaseType.UINT32, 2, 0, "cycles", null),
new FitMessageFieldDefinition("monitoring_time_source", 8, 1, FitFieldBaseType.ENUM, null)
);
public static final FitMessageDefinition DEFINITION_CONNECTIVITY = new FitMessageDefinition(FIT_MESSAGE_NAME_CONNECTIVITY, FIT_MESSAGE_NUMBER_CONNECTIVITY, MESSAGE_ID_CONNECTIVITY,
new FitMessageFieldDefinition("bluetooth_enabled", 0, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("bluetooth_le_enabled", 1, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("ant_enabled", 2, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("name", 3, 16, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("live_tracking_enabled", 4, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("weather_conditions_enabled", 5, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("weather_alerts_enabled", 6, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("auto_activity_upload_enabled", 7, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("course_download_enabled", 8, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("workout_download_enabled", 9, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("gps_ephemeris_download_enabled", 10, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("live_track_auto_start_enabled", 13, 1, FitFieldBaseType.ENUM, FitBool.FALSE)
);
public static final FitMessageDefinition DEFINITION_WEATHER_CONDITIONS = new FitMessageDefinition(FIT_MESSAGE_NAME_WEATHER_CONDITIONS, FIT_MESSAGE_NUMBER_WEATHER_CONDITIONS, MESSAGE_ID_WEATHER_CONDITIONS,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("weather_report", 0, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("temperature", 1, 1, FitFieldBaseType.SINT8, 1, 0, "°C", null),
new FitMessageFieldDefinition("condition", 2, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("wind_direction", 3, 2, FitFieldBaseType.UINT16, 1, 0, "°", null),
new FitMessageFieldDefinition("wind_speed", 4, 2, FitFieldBaseType.UINT16, 1000, 0, "m/s", null),
new FitMessageFieldDefinition("precipitation_probability", 5, 1, FitFieldBaseType.UINT8, 1, 0, "%", null),
new FitMessageFieldDefinition("temperature_feels_like", 6, 1, FitFieldBaseType.SINT8, 1, 0, "°C", null),
new FitMessageFieldDefinition("relative_humidity", 7, 1, FitFieldBaseType.UINT8, 1, 0, "%", null),
new FitMessageFieldDefinition("location", 8, 16, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("observed_at_time", 9, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("observed_location_lat", 10, 4, FitFieldBaseType.SINT32, 1, 0, "semicircles", null),
new FitMessageFieldDefinition("observed_location_long", 11, 4, FitFieldBaseType.SINT32, 1, 0, "semicircles", null),
new FitMessageFieldDefinition("day_of_week", 12, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("high_temperature", 13, 1, FitFieldBaseType.SINT8, 1, 0, "°C", null),
new FitMessageFieldDefinition("low_temperature", 14, 1, FitFieldBaseType.SINT8, 1, 0, "°C", null)
);
public static final FitMessageDefinition DEFINITION_WEATHER_ALERT = new FitMessageDefinition(FIT_MESSAGE_NAME_WEATHER_ALERT, FIT_MESSAGE_NUMBER_WEATHER_ALERT, MESSAGE_ID_WEATHER_ALERT,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("report_id", 0, 10, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("issue_time", 1, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("expire_time", 2, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("severity", 3, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("type", 4, 1, FitFieldBaseType.ENUM, null)
);
public static final FitMessageDefinition DEFINITION_FILE_DESCRIPTION = new FitMessageDefinition(FIT_MESSAGE_NAME_FILE_DESCRIPTION, FIT_MESSAGE_NUMBER_FILE_DESCRIPTION, -1,
new FitMessageFieldDefinition("message_index", 254, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("manufacturer", 0, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("directory", 1, 16, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("name", 2, 20, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("flags", 3, 1, FitFieldBaseType.UINT8Z, null),
new FitMessageFieldDefinition("purpose", 4, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("garmin_file_purpose", 5, 1, FitFieldBaseType.UINT8, null)
);
public static final FitMessageDefinition DEFINITION_OHR_SETTINGS = new FitMessageDefinition(FIT_MESSAGE_NAME_OHR_SETTINGS, FIT_MESSAGE_NUMBER_OHR_SETTINGS, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("enabled", 0, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("sample_rate", 1, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("transmit_hr_enabled", 2, 1, FitFieldBaseType.ENUM, FitBool.FALSE)
);
public static final FitMessageDefinition DEFINITION_EXD_SCREEN_CONFIGURATION = new FitMessageDefinition(FIT_MESSAGE_NAME_EXD_SCREEN_CONFIGURATION, FIT_MESSAGE_NUMBER_EXD_SCREEN_CONFIGURATION, -1,
new FitMessageFieldDefinition("screen_index", 0, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("field_count", 1, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("layout", 2, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("screen_enabled", 3, 1, FitFieldBaseType.ENUM, null)
);
public static final FitMessageDefinition DEFINITION_EXD_DATA_FIELD_CONFIGURATION = new FitMessageDefinition(FIT_MESSAGE_NAME_EXD_DATA_FIELD_CONFIGURATION, FIT_MESSAGE_NUMBER_EXD_DATA_FIELD_CONFIGURATION, -1,
new FitMessageFieldDefinition("screen_index", 0, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("concept_field", 1, 1, FitFieldBaseType.BYTE, null),
new FitMessageFieldDefinition("field_id", 2, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("concept_count", 3, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("display_type", 4, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("title", 5, 32, FitFieldBaseType.STRING, null)
);
public static final FitMessageDefinition DEFINITION_EXD_DATA_CONCEPT_CONFIGURATION = new FitMessageDefinition(FIT_MESSAGE_NAME_EXD_DATA_CONCEPT_CONFIGURATION, FIT_MESSAGE_NUMBER_EXD_DATA_CONCEPT_CONFIGURATION, -1,
new FitMessageFieldDefinition("screen_index", 0, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("concept_field", 1, 1, FitFieldBaseType.BYTE, null),
new FitMessageFieldDefinition("field_id", 2, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("concept_index", 3, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("data_page", 4, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("concept_key", 5, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("scaling", 6, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("offset", 7, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("data_units", 8, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("qualifier", 9, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("descriptor", 10, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("is_signed", 11, 1, FitFieldBaseType.ENUM, FitBool.FALSE)
);
public static final FitMessageDefinition DEFINITION_MONITORING_HR_DATA = new FitMessageDefinition(FIT_MESSAGE_NAME_MONITORING_HR_DATA, FIT_MESSAGE_NUMBER_MONITORING_HR_DATA, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("resting_heart_rate", 0, 1, FitFieldBaseType.UINT8, 1, 0, "bpm", null),
new FitMessageFieldDefinition("current_day_resting_heart_rate", 1, 1, FitFieldBaseType.UINT8, 1, 0, "bpm", null)
);
public static final FitMessageDefinition DEFINITION_ALARM_SETTINGS = new FitMessageDefinition(FIT_MESSAGE_NAME_ALARM_SETTINGS, FIT_MESSAGE_NUMBER_ALARM_SETTINGS, -1,
new FitMessageFieldDefinition("message_index", 254, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("time", 0, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("days", 1, 1, FitFieldBaseType.BYTE, null),
new FitMessageFieldDefinition("enabled", 2, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("sound", 3, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("backlight", 4, 1, FitFieldBaseType.ENUM, FitBool.FALSE),
new FitMessageFieldDefinition("id", 5, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("alarm_mesg", 6, 20, FitFieldBaseType.STRING, null),
new FitMessageFieldDefinition("snooze_count", 7, 1, FitFieldBaseType.UINT8, null)
);
public static final FitMessageDefinition DEFINITION_STRESS_LEVEL = new FitMessageDefinition(FIT_MESSAGE_NAME_STRESS_LEVEL, FIT_MESSAGE_NUMBER_STRESS_LEVEL, -1,
new FitMessageFieldDefinition("stress_level_value", 0, 2, FitFieldBaseType.SINT16, null),
new FitMessageFieldDefinition("stress_level_time", 1, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("average_stress_intensity", 2, 1, FitFieldBaseType.SINT8, null)
);
public static final FitMessageDefinition DEFINITION_MANUAL_STRESS_LEVEL = new FitMessageDefinition(FIT_MESSAGE_NAME_MANUAL_STRESS_LEVEL, FIT_MESSAGE_NUMBER_MANUAL_STRESS_LEVEL, -1,
new FitMessageFieldDefinition("stress_level_value", 0, 2, FitFieldBaseType.SINT16, null),
new FitMessageFieldDefinition("stress_level_time", 1, 4, FitFieldBaseType.UINT32, null)
);
public static final FitMessageDefinition DEFINITION_MAX_MET_DATA = new FitMessageDefinition(FIT_MESSAGE_NAME_MAX_MET_DATA, FIT_MESSAGE_NUMBER_MAX_MET_DATA, -1,
new FitMessageFieldDefinition("update_time", 0, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("max_met", 1, 4, FitFieldBaseType.SINT32, null),
new FitMessageFieldDefinition("vo2_max", 2, 2, FitFieldBaseType.UINT16, 10, 0, "mL/kg/min", null),
new FitMessageFieldDefinition("fitness_age", 3, 1, FitFieldBaseType.SINT8, null),
new FitMessageFieldDefinition("fitness_age_desc", 4, 1, FitFieldBaseType.SINT8, null),
new FitMessageFieldDefinition("sport", 5, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("sub_sport", 6, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("analyzer_method", 7, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("max_met_category", 8, 1, FitFieldBaseType.ENUM, null),
new FitMessageFieldDefinition("calibrated_data", 9, 1, FitFieldBaseType.ENUM, null)
);
public static final FitMessageDefinition DEFINITION_WHR_DIAG = new FitMessageDefinition(FIT_MESSAGE_NAME_WHR_DIAG, FIT_MESSAGE_NUMBER_WHR_DIAG, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("fractional_timestamp", 1, 2, FitFieldBaseType.UINT16, 32768.0, 0, "s", null),
new FitMessageFieldDefinition("page_data", 2, 1, FitFieldBaseType.BYTE, null)
);
public static final FitMessageDefinition DEFINITION_METRICS_INFO = new FitMessageDefinition(FIT_MESSAGE_NAME_METRICS_INFO, FIT_MESSAGE_NUMBER_METRICS_INFO, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("local_timestamp", 0, 4, FitFieldBaseType.UINT32, null)
);
public static final FitMessageDefinition DEFINITION_PAGES_MAP = new FitMessageDefinition(FIT_MESSAGE_NAME_PAGES_MAP, FIT_MESSAGE_NUMBER_PAGES_MAP, -1,
new FitMessageFieldDefinition("message_index", 254, 2, FitFieldBaseType.UINT16, null),
new FitMessageFieldDefinition("map", 0, 10, FitFieldBaseType.BYTE, null),
new FitMessageFieldDefinition("default_to_last", 1, 1, FitFieldBaseType.ENUM, FitBool.FALSE)
);
public static final FitMessageDefinition DEFINITION_NEURAL_NETWORK_INFO = new FitMessageDefinition(FIT_MESSAGE_NAME_NEURAL_NETWORK_INFO, FIT_MESSAGE_NUMBER_NEURAL_NETWORK_INFO, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("network_version", 0, 1, FitFieldBaseType.UINT8, null),
new FitMessageFieldDefinition("implicit_message_duration", 1, 2, FitFieldBaseType.UINT16, 1, 0, "s", null),
new FitMessageFieldDefinition("local_timestamp", 2, 4, FitFieldBaseType.UINT32, null)
);
public static final FitMessageDefinition DEFINITION_NEURAL_NETWORK_DATA = new FitMessageDefinition(FIT_MESSAGE_NAME_NEURAL_NETWORK_DATA, FIT_MESSAGE_NUMBER_NEURAL_NETWORK_DATA, -1,
new FitMessageFieldDefinition("network_data", 0, 20, FitFieldBaseType.BYTE, null)
);
public static final FitMessageDefinition DEFINITION_SLEEP_LEVEL = new FitMessageDefinition(FIT_MESSAGE_NAME_SLEEP_LEVEL, FIT_MESSAGE_NUMBER_SLEEP_LEVEL, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null),
new FitMessageFieldDefinition("sleep_level", 0, 1, FitFieldBaseType.ENUM, null)
);
public static final FitMessageDefinition DEFINITION_END_OF_FILE = new FitMessageDefinition(FIT_MESSAGE_NAME_END_OF_FILE, FIT_MESSAGE_NUMBER_END_OF_FILE, -1,
new FitMessageFieldDefinition("timestamp", 253, 4, FitFieldBaseType.UINT32, null)
);
public static final List<FitMessageDefinition> ALL_DEFINITIONS = Arrays.asList(
DEFINITION_FILE_ID,
DEFINITION_CAPABILITIES,
DEFINITION_DEVICE_SETTINGS,
DEFINITION_USER_PROFILE,
DEFINITION_EVENT,
DEFINITION_DEVICE_INFO,
DEFINITION_DEBUG,
DEFINITION_SOFTWARE,
DEFINITION_FILE_CAPABILITIES,
DEFINITION_FILE_CREATOR,
DEFINITION_MONITORING,
DEFINITION_MONITORING_INFO,
DEFINITION_CONNECTIVITY,
DEFINITION_WEATHER_CONDITIONS,
DEFINITION_WEATHER_ALERT,
DEFINITION_FILE_DESCRIPTION,
DEFINITION_EXD_SCREEN_CONFIGURATION,
DEFINITION_EXD_DATA_FIELD_CONFIGURATION,
DEFINITION_EXD_DATA_CONCEPT_CONFIGURATION,
DEFINITION_OHR_SETTINGS,
DEFINITION_MONITORING_HR_DATA,
DEFINITION_ALARM_SETTINGS,
DEFINITION_STRESS_LEVEL,
DEFINITION_MANUAL_STRESS_LEVEL,
DEFINITION_MAX_MET_DATA,
DEFINITION_WHR_DIAG,
DEFINITION_METRICS_INFO,
DEFINITION_PAGES_MAP,
DEFINITION_NEURAL_NETWORK_INFO,
DEFINITION_NEURAL_NETWORK_DATA,
DEFINITION_SLEEP_LEVEL,
DEFINITION_END_OF_FILE
);
}

View File

@ -0,0 +1,35 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageWriter;
public class FitMessageFieldDefinition {
public final String fieldName;
public final int fieldNumber;
public final int fieldSize;
public final FitFieldBaseType fieldType;
public final double scale;
public final double offset;
public final String units;
public final Object defaultValue;
public FitMessageFieldDefinition(String fieldName, int fieldNumber, int fieldSize, FitFieldBaseType fieldType, Object defaultValue) {
this(fieldName, fieldNumber, fieldSize, fieldType, 0, 0, null, defaultValue);
}
public FitMessageFieldDefinition(String fieldName, int fieldNumber, int fieldSize, FitFieldBaseType fieldType, double scale, double offset, String units, Object defaultValue) {
this.fieldName = fieldName;
this.fieldNumber = fieldNumber;
this.fieldSize = fieldSize;
this.fieldType = fieldType;
this.scale = scale;
this.offset = offset;
this.units = units;
this.defaultValue = defaultValue == null ? fieldType.invalidValue : defaultValue;
}
public void writeToMessage(MessageWriter writer) {
writer.writeByte(fieldNumber);
writer.writeByte(fieldSize);
writer.writeByte(fieldType.typeID);
}
}

View File

@ -0,0 +1,285 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import android.util.SparseArray;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageReader;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class FitParser {
private static final Logger LOG = LoggerFactory.getLogger(FitParser.class);
// .FIT magic value indicating a .FIT file
private static final int FIT_MAGIC = 0x5449462E;
private static final int FLAG_NORMAL_HEADER = 0x80;
private static final int FLAG_DEFINITION_MESSAGE = 0x40;
private static final int FLAG_DEVELOPER_FIELDS = 0x20;
private static final int MASK_LOCAL_MESSAGE_TYPE = 0x0F;
private static final int MASK_TIME_OFFSET = 0x1F;
private static final int MASK_COMPRESSED_LOCAL_MESSAGE_TYPE = 0x60;
private final SparseArray<FitMessageDefinition> globalMessageDefinitions;
private final SparseArray<FitLocalMessageDefinition> localMessageDefinitions = new SparseArray<>(16);
public FitParser(Collection<FitMessageDefinition> knownDefinitions) {
globalMessageDefinitions = new SparseArray<>(knownDefinitions.size());
for (FitMessageDefinition definition : knownDefinitions) {
globalMessageDefinitions.append(definition.globalMessageID, definition);
}
}
public SparseArray<FitLocalMessageDefinition> getLocalMessageDefinitions() {
return localMessageDefinitions;
}
public List<FitMessage> parseFitFile(byte[] data) {
if (data.length < 12) throw new IllegalArgumentException("Too short data");
final MessageReader reader = new MessageReader(data);
final List<FitMessage> result = new ArrayList<>();
while (!reader.isEof()) {
final int fileHeaderStart = reader.getPosition();
final int fileHeaderSize = reader.readByte();
final int protocolVersion = reader.readByte();
final int profileVersion = reader.readShort();
final int dataSize = reader.readInt();
final int dataTypeMagic = reader.readInt();
final int headerCrc = fileHeaderSize >= 14 ? reader.readShort() : 0;
if (dataTypeMagic != FIT_MAGIC) {
throw new IllegalArgumentException("Not a FIT file, data type signature not found");
}
if (fileHeaderSize < 12) throw new IllegalArgumentException("Header size too low");
reader.skip(fileHeaderSize - 14);
// TODO: Check header CRC
localMessageDefinitions.clear();
int lastTimestamp = 0;
final int end = fileHeaderStart + fileHeaderSize + dataSize;
while (reader.getPosition() < end) {
final int recordHeader = reader.readByte();
final boolean isDefinitionMessage;
final int localMessageType;
final int currentTimestamp;
if ((recordHeader & FLAG_NORMAL_HEADER) == 0) {
// normal header
isDefinitionMessage = (recordHeader & FLAG_DEFINITION_MESSAGE) != 0;
localMessageType = recordHeader & MASK_LOCAL_MESSAGE_TYPE;
currentTimestamp = -1;
} else {
// compressed timestamp header
final int timestampOffset = recordHeader & MASK_TIME_OFFSET;
localMessageType = (recordHeader & MASK_COMPRESSED_LOCAL_MESSAGE_TYPE) >> 4;
currentTimestamp = lastTimestamp + timestampOffset;
isDefinitionMessage = false;
throw new IllegalArgumentException("Compressed timestamps not supported yet");
}
if (isDefinitionMessage) {
final boolean hasDeveloperFields = (recordHeader & FLAG_DEVELOPER_FIELDS) != 0;
final FitLocalMessageDefinition definition = parseDefinitionMessage(reader, hasDeveloperFields);
LOG.trace("Defining local message {} to global message {}", localMessageType, definition.globalDefinition.globalMessageID);
localMessageDefinitions.put(localMessageType, definition);
} else {
final FitLocalMessageDefinition definition = localMessageDefinitions.get(localMessageType);
if (definition == null) {
LOG.error("Use of undefined local message {}", localMessageType);
throw new IllegalArgumentException("Use of undefined local message " + localMessageType);
}
final FitMessage dataMessage = new FitMessage(definition.globalDefinition);
parseDataMessage(reader, definition, dataMessage);
result.add(dataMessage);
}
}
final int fileCrc = reader.readShort();
// TODO: Check file CRC
}
return result;
}
private void parseDataMessage(MessageReader reader, FitLocalMessageDefinition localMessageDefinition, FitMessage dataMessage) {
for (FitLocalFieldDefinition localFieldDefinition : localMessageDefinition.fieldDefinitions) {
final Object value = readValue(reader, localFieldDefinition);
if (!localFieldDefinition.baseType.invalidValue.equals(value)) {
dataMessage.setField(localFieldDefinition.globalDefinition.fieldNumber, value);
}
}
}
private Object readValue(MessageReader reader, FitLocalFieldDefinition fieldDefinition) {
//switch (fieldDefinition.baseType) {
switch (fieldDefinition.globalDefinition.fieldType) {
case ENUM:
case SINT8:
case UINT8:
case SINT16:
case UINT16:
case SINT32:
case UINT32:
case UINT8Z:
case UINT16Z:
case UINT32Z:
case SINT64:
case UINT64:
case UINT64Z:
return readFitNumber(reader, fieldDefinition.size, fieldDefinition.globalDefinition.scale, fieldDefinition.globalDefinition.offset);
case BYTE:
return fieldDefinition.size == 1 ? reader.readByte() : reader.readBytes(fieldDefinition.size);
case STRING:
return readFitString(reader, fieldDefinition.size);
case FLOAT32:
return readFloat32(reader, fieldDefinition.size);
case FLOAT64:
return readFloat64(reader, fieldDefinition.size);
// TODO: Float data types
default:
throw new IllegalArgumentException("Unable to read value of type " + fieldDefinition.baseType);
}
}
private float readFloat32(MessageReader reader, int size) {
if (size != 4) {
throw new IllegalArgumentException("Invalid size for Float32: " + size);
}
final byte[] bytes = reader.readBytes(size);
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getFloat();
}
private double readFloat64(MessageReader reader, int size) {
if (size != 8) {
throw new IllegalArgumentException("Invalid size for Float64: " + size);
}
final byte[] bytes = reader.readBytes(size);
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getDouble();
}
private String readFitString(MessageReader reader, int size) {
final byte[] bytes = reader.readBytes(size);
final int zero = ArrayUtils.indexOf((byte) 0, bytes);
if (zero < 0) {
LOG.warn("Unterminated string");
return new String(bytes, StandardCharsets.UTF_8);
}
return new String(bytes, 0, zero, StandardCharsets.UTF_8);
}
private Object readRawFitNumber(MessageReader reader, int size) {
switch (size) {
case 1:
return reader.readByte();
case 2:
return reader.readShort();
case 3: {
// this is strange?
byte[] bytes = new byte[4];
reader.readBytesTo(3, bytes, 0);
return BinaryUtils.readInt(bytes, 0);
}
case 4:
return reader.readInt();
case 7: {
// this is strange?
byte[] bytes = new byte[8];
reader.readBytesTo(7, bytes, 0);
return BinaryUtils.readLong(bytes, 0);
}
case 8:
return reader.readLong();
case 12:
// this is strange?
long lower = reader.readLong();
int upper = reader.readInt();
return upper * ((double) Long.MAX_VALUE) + lower;
case 16:
// this is strange?
return reader.readLong() + reader.readLong() * (double) (Long.MAX_VALUE);
case 32:
// this is strange?
// TODO: FIXME: 32-byte integer?!?
reader.skip(16);
return Math.pow(2, 128) * (reader.readLong() + reader.readLong() * (double) (Long.MAX_VALUE));
default:
throw new IllegalArgumentException("Unable to read number of size " + size);
}
}
private Object readFitNumber(MessageReader reader, int size, double scale, double offset) {
if (scale == 0) {
return readRawFitNumber(reader, size);
} else {
switch (size) {
case 1:
return reader.readByte() / scale + offset;
case 2:
return reader.readShort() / scale + offset;
case 4:
return reader.readInt() / scale + offset;
case 8:
return reader.readLong() / scale + offset;
default:
throw new IllegalArgumentException("Unable to read number of size " + size);
}
}
}
private FitLocalMessageDefinition parseDefinitionMessage(MessageReader reader, boolean hasDeveloperFields) {
reader.skip(1);
final int architecture = reader.readByte();
final boolean isBigEndian = architecture == 1;
if (isBigEndian) throw new IllegalArgumentException("Big-endian data not supported yet");
final int globalMessageType = reader.readShort();
final FitMessageDefinition messageDefinition = getGlobalDefinition(globalMessageType);
final int fieldCount = reader.readByte();
final List<FitLocalFieldDefinition> fields = new ArrayList<>(fieldCount);
for (int i = 0; i < fieldCount; ++i) {
final int globalField = reader.readByte();
final int size = reader.readByte();
final int baseTypeNum = reader.readByte();
final FitFieldBaseType baseType = FitFieldBaseType.decodeTypeID(baseTypeNum);
final FitMessageFieldDefinition globalFieldDefinition = getFieldDefinition(messageDefinition, globalField, size, baseType);
fields.add(new FitLocalFieldDefinition(globalFieldDefinition, size, baseType));
}
if (hasDeveloperFields) {
final int developerFieldCount = reader.readByte();
if (developerFieldCount != 0) throw new IllegalArgumentException("Developer fields not supported yet");
}
return new FitLocalMessageDefinition(messageDefinition, fields);
}
private FitMessageFieldDefinition getFieldDefinition(FitMessageDefinition messageDefinition, int field, int size, FitFieldBaseType baseType) {
final FitMessageFieldDefinition definition = messageDefinition.getField(field);
if (definition != null) return definition;
LOG.warn("Unknown field {} in message {}", field, messageDefinition.globalMessageID);
// System.out.println(String.format(Locale.ROOT, "Unknown field %d in message %d", field, messageDefinition.globalMessageID));
final FitMessageFieldDefinition newDefinition = new FitMessageFieldDefinition("unknown_" + field, field, size, baseType, baseType.invalidValue);
messageDefinition.addField(newDefinition);
return newDefinition;
}
private FitMessageDefinition getGlobalDefinition(int globalMessageType) {
final FitMessageDefinition messageDefinition = globalMessageDefinitions.get(globalMessageType);
if (messageDefinition != null) return messageDefinition;
LOG.warn("Unknown global message {}", globalMessageType);
// System.out.println(String.format(Locale.ROOT, "Unknown message %d", globalMessageType));
final FitMessageDefinition newDefinition = new FitMessageDefinition("unknown_" + globalMessageType, globalMessageType, 0);
globalMessageDefinitions.append(globalMessageType, newDefinition);
return newDefinition;
}
}

View File

@ -0,0 +1,258 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class FitSerializer {
private static final Logger LOG = LoggerFactory.getLogger(FitSerializer.class);
private final SparseBooleanArray knownMessageIDs = new SparseBooleanArray(16);
private final SparseIntArray localMessageIDs = new SparseIntArray(16);
private final SparseArray<FitLocalMessageDefinition> localMessageDefinitions;
// .FIT magic value indicating a .FIT file
private static final int FIT_MAGIC = 0x5449462E;
private static final int FLAG_NORMAL_HEADER = 0x80;
private static final int FLAG_DEFINITION_MESSAGE = 0x40;
private static final int FLAG_DEVELOPER_FIELDS = 0x20;
private static final int MASK_LOCAL_MESSAGE_TYPE = 0x0F;
private static final int MASK_TIME_OFFSET = 0x1F;
private static final int MASK_COMPRESSED_LOCAL_MESSAGE_TYPE = 0x60;
public FitSerializer() {
this(new SparseArray<FitLocalMessageDefinition>(16));
}
public FitSerializer(SparseArray<FitLocalMessageDefinition> initialDefinitions) {
this.localMessageDefinitions = initialDefinitions;
for (int i = 0; i < initialDefinitions.size(); ++i) {
final int localId = initialDefinitions.keyAt(i);
final FitLocalMessageDefinition definition = initialDefinitions.valueAt(i);
knownMessageIDs.put(definition.globalDefinition.globalMessageID, true);
localMessageIDs.put(definition.globalDefinition.globalMessageID, localId);
}
}
public byte[] serializeFitFile(List<FitMessage> messages) {
final MessageWriter writer = new MessageWriter();
writer.writeByte(14);
writer.writeByte(0x10);
writer.writeShort(2057);
// dataSize will be rewritten later
writer.writeInt(0);
writer.writeInt(FIT_MAGIC);
// CRC will be rewritten later
writer.writeShort(0);
// first, gather additional needed definitions (if any)
for (final FitMessage message : messages) {
final FitMessageDefinition messageDefinition = message.definition;
final int globalMessageID = messageDefinition.globalMessageID;
if (!knownMessageIDs.get(globalMessageID)) {
LOG.debug("FitSerializer needs to add definition for {}", globalMessageID);
final int localMessageID = localMessageIDs.size() == 0 ? 0 : localMessageIDs.keyAt(localMessageIDs.size() - 1) + 1;
localMessageIDs.put(globalMessageID, localMessageID);
knownMessageIDs.put(globalMessageID, true);
final List<FitMessageFieldDefinition> fieldDefinitions = messageDefinition.fieldDefinitions;
final List<FitLocalFieldDefinition> localFieldDefinitions = new ArrayList<>(fieldDefinitions.size());
for (FitMessageFieldDefinition definition : fieldDefinitions) {
localFieldDefinitions.add(new FitLocalFieldDefinition(definition, definition.fieldSize, definition.fieldType));
}
localMessageDefinitions.put(localMessageID, new FitLocalMessageDefinition(messageDefinition, localFieldDefinitions));
}
}
// now, write definition messages for all used message types
final SparseBooleanArray definedMessages = new SparseBooleanArray();
for (final FitMessage message : messages) {
int localMessageID = localMessageIDs.get(message.definition.globalMessageID);
if (!definedMessages.get(localMessageID)) {
definedMessages.put(localMessageID, true);
writeDefinitionMessage(writer, localMessageID, localMessageDefinitions.get(localMessageID));
}
}
// and now, write the data messages
for (final FitMessage message : messages) {
int localMessageID = localMessageIDs.get(message.definition.globalMessageID);
final FitLocalMessageDefinition localMessageDefinition = localMessageDefinitions.get(localMessageID);
writeDataMessage(writer, message, localMessageID, localMessageDefinition);
}
writer.writeShort(ChecksumCalculator.computeCrc(writer.peekBytes(), 14, writer.getSize() - 14));
final byte[] bytes = writer.getBytes();
// rewrite size
BinaryUtils.writeInt(bytes, 4, bytes.length - 14 - 2);
// rewrite header CRC
BinaryUtils.writeShort(bytes, 12, ChecksumCalculator.computeCrc(bytes, 0, 12));
return bytes;
}
private void writeDefinitionMessage(MessageWriter writer, int localMessageID, FitLocalMessageDefinition localMessageDefinition) {
writer.writeByte(FLAG_DEFINITION_MESSAGE | localMessageID);
writer.writeByte(0);
writer.writeByte(0);
writer.writeShort(localMessageDefinition.globalDefinition.globalMessageID);
writer.writeByte(localMessageDefinition.fieldDefinitions.size());
for (FitLocalFieldDefinition localFieldDefinition : localMessageDefinition.fieldDefinitions) {
writer.writeByte(localFieldDefinition.globalDefinition.fieldNumber);
writer.writeByte(localFieldDefinition.size);
writer.writeByte(localFieldDefinition.baseType.typeID);
}
}
private void writeDataMessage(MessageWriter writer, FitMessage message, int localMessageID, FitLocalMessageDefinition localMessageDefinition) {
writer.writeByte(localMessageID);
for (FitLocalFieldDefinition localFieldDefinition : localMessageDefinition.fieldDefinitions) {
Object value = message.getField(localFieldDefinition.globalDefinition.fieldNumber);
if (value == null) {
value = localFieldDefinition.baseType.invalidValue;
}
writeValue(writer, localFieldDefinition, value);
}
}
private void writeValue(MessageWriter writer, FitLocalFieldDefinition fieldDefinition, Object value) {
switch (fieldDefinition.globalDefinition.fieldType) {
case ENUM:
case SINT8:
case UINT8:
case SINT16:
case UINT16:
case SINT32:
case UINT32:
case UINT8Z:
case UINT16Z:
case UINT32Z:
case SINT64:
case UINT64:
case UINT64Z:
writeFitNumber(writer, value, fieldDefinition.size, fieldDefinition.globalDefinition.scale, fieldDefinition.globalDefinition.offset);
break;
case BYTE:
if (fieldDefinition.size == 1) {
writer.writeByte((int) value);
} else {
writer.writeBytes((byte[]) value);
}
break;
case STRING:
writeFitString(writer, (String) value, fieldDefinition.size);
break;
case FLOAT32:
writeFloat32(writer, (float) value);
break;
case FLOAT64:
writeFloat64(writer, (double) value);
break;
default:
throw new IllegalArgumentException("Unable to write value of type " + fieldDefinition.baseType);
}
}
private void writeFitString(MessageWriter writer, String value, int size) {
if (value.length() >= size) throw new IllegalArgumentException("Too long string");
final byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
writer.writeBytes(bytes);
final byte[] zeroes = new byte[size - value.length()];
writer.writeBytes(zeroes);
}
private void writeFloat32(MessageWriter writer, float value) {
writer.writeBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putFloat(value).array(), 0, 4);
}
private void writeFloat64(MessageWriter writer, double value) {
writer.writeBytes(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putDouble(value).array(), 0, 4);
}
private void writeFitNumber(MessageWriter writer, Object value, int size, double scale, double offset) {
if (scale == 0) {
writeRawFitNumber(writer, value, size);
} else {
final long rawValue = Math.round((double) value * scale - offset);
switch (size) {
case 1:
writer.writeByte((int) rawValue);
break;
case 2:
writer.writeShort((int) rawValue);
break;
case 4:
writer.writeInt((int) rawValue);
break;
case 8:
writer.writeLong(rawValue);
break;
default:
throw new IllegalArgumentException("Unable to write number of size " + size);
}
}
}
private void writeRawFitNumber(MessageWriter writer, Object value, int size) {
switch (size) {
case 1:
writer.writeByte((int) value);
break;
case 2:
writer.writeShort((int) value);
break;
case 3: {
// this is strange?
byte[] bytes = new byte[4];
BinaryUtils.writeInt(bytes, 0, (int) value);
writer.writeBytes(bytes, 0, 3);
break;
}
case 4:
writer.writeInt((int) value);
break;
case 7: {
// this is strange?
byte[] bytes = new byte[8];
BinaryUtils.writeLong(bytes, 0, (long) value);
writer.writeBytes(bytes, 0, 7);
break;
}
case 8:
writer.writeLong((long) value);
break;
case 12: {
// this is strange? (and probably losing precision anyway)
final double val = (double) value;
final long upper = Math.round(val / Long.MAX_VALUE);
final long lower = Math.round(val - upper);
writer.writeLong(lower);
writer.writeInt((int) upper);
break;
}
case 16: {
// this is strange? (and probably losing precision anyway)
final double val = (double) value;
final long upper = Math.round(val / Long.MAX_VALUE);
final long lower = Math.round(val - upper);
writer.writeLong(lower);
writer.writeLong(upper);
break;
}
default:
throw new IllegalArgumentException("Unable to read number of size " + size);
}
}
}

View File

@ -0,0 +1,191 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
public final class FitWeatherConditions {
public static final int CLEAR = 0;
public static final int PARTLY_CLOUDY = 1;
public static final int MOSTLY_CLOUDY = 2;
public static final int RAIN = 3;
public static final int SNOW = 4;
public static final int WINDY = 5;
public static final int THUNDERSTORMS = 6;
public static final int WINTRY_MIX = 7;
public static final int FOG = 8;
public static final int HAZY = 11;
public static final int HAIL = 12;
public static final int SCATTERED_SHOWERS = 13;
public static final int SCATTERED_THUNDERSTORMS = 14;
public static final int UNKNOWN_PRECIPITATION = 15;
public static final int LIGHT_RAIN = 16;
public static final int HEAVY_RAIN = 17;
public static final int LIGHT_SNOW = 18;
public static final int HEAVY_SNOW = 19;
public static final int LIGHT_RAIN_SNOW = 20;
public static final int HEAVY_RAIN_SNOW = 21;
public static final int CLOUDY = 22;
public static final int ALERT_SEVERITY_UNKNOWN = 0;
public static final int ALERT_SEVERITY_WARNING = 1;
public static final int ALERT_SEVERITY_WATCH = 2;
public static final int ALERT_SEVERITY_ADVISORY = 3;
public static final int ALERT_SEVERITY_STATEMENT = 4;
public static final int ALERT_TYPE_UNSPECIFIED = 0;
public static final int ALERT_TYPE_TORNADO = 1;
public static final int ALERT_TYPE_TSUNAMI = 2;
public static final int ALERT_TYPE_HURRICANE = 3;
public static final int ALERT_TYPE_EXTREME_WIND = 4;
public static final int ALERT_TYPE_TYPHOON = 5;
public static final int ALERT_TYPE_INLAND_HURRICANE = 6;
public static final int ALERT_TYPE_HURRICANE_FORCE_WIND = 7;
public static final int ALERT_TYPE_WATERSPOUT = 8;
public static final int ALERT_TYPE_SEVERE_THUNDERSTORM = 9;
public static final int ALERT_TYPE_WRECKHOUSE_WINDS = 10;
public static final int ALERT_TYPE_LES_SUETES_WIND = 11;
public static final int ALERT_TYPE_AVALANCHE = 12;
public static final int ALERT_TYPE_FLASH_FLOOD = 13;
public static final int ALERT_TYPE_TROPICAL_STORM = 14;
public static final int ALERT_TYPE_INLAND_TROPICAL_STORM = 15;
public static final int ALERT_TYPE_BLIZZARD = 16;
public static final int ALERT_TYPE_ICE_STORM = 17;
public static final int ALERT_TYPE_FREEZING_RAIN = 18;
public static final int ALERT_TYPE_DEBRIS_FLOW = 19;
public static final int ALERT_TYPE_FLASH_FREEZE = 20;
public static final int ALERT_TYPE_DUST_STORM = 21;
public static final int ALERT_TYPE_HIGH_WIND = 22;
public static final int ALERT_TYPE_WINTER_STORM = 23;
public static final int ALERT_TYPE_HEAVY_FREEZING_SPRAY = 24;
public static final int ALERT_TYPE_EXTREME_COLD = 25;
public static final int ALERT_TYPE_WIND_CHILL = 26;
public static final int ALERT_TYPE_COLD_WAVE = 27;
public static final int ALERT_TYPE_HEAVY_SNOW_ALERT = 28;
public static final int ALERT_TYPE_LAKE_EFFECT_BLOWING_SNOW = 29;
public static final int ALERT_TYPE_SNOW_SQUALL = 30;
public static final int ALERT_TYPE_LAKE_EFFECT_SNOW = 31;
public static final int ALERT_TYPE_WINTER_WEATHER = 32;
public static final int ALERT_TYPE_SLEET = 33;
public static final int ALERT_TYPE_SNOWFALL = 34;
public static final int ALERT_TYPE_SNOW_AND_BLOWING_SNOW = 35;
public static final int ALERT_TYPE_BLOWING_SNOW = 36;
public static final int ALERT_TYPE_SNOW_ALERT = 37;
public static final int ALERT_TYPE_ARCTIC_OUTFLOW = 38;
public static final int ALERT_TYPE_FREEZING_DRIZZLE = 39;
public static final int ALERT_TYPE_STORM = 40;
public static final int ALERT_TYPE_STORM_SURGE = 41;
public static final int ALERT_TYPE_RAINFALL = 42;
public static final int ALERT_TYPE_AREAL_FLOOD = 43;
public static final int ALERT_TYPE_COASTAL_FLOOD = 44;
public static final int ALERT_TYPE_LAKESHORE_FLOOD = 45;
public static final int ALERT_TYPE_EXCESSIVE_HEAT = 46;
public static final int ALERT_TYPE_HEAT = 47;
public static final int ALERT_TYPE_WEATHER = 48;
public static final int ALERT_TYPE_HIGH_HEAT_AND_HUMIDITY = 49;
public static final int ALERT_TYPE_HUMIDEX_AND_HEALTH = 50;
public static final int ALERT_TYPE_HUMIDEX = 51;
public static final int ALERT_TYPE_GALE = 52;
public static final int ALERT_TYPE_FREEZING_SPRAY = 53;
public static final int ALERT_TYPE_SPECIAL_MARINE = 54;
public static final int ALERT_TYPE_SQUALL = 55;
public static final int ALERT_TYPE_STRONG_WIND = 56;
public static final int ALERT_TYPE_LAKE_WIND = 57;
public static final int ALERT_TYPE_MARINE_WEATHER = 58;
public static final int ALERT_TYPE_WIND = 59;
public static final int ALERT_TYPE_SMALL_CRAFT_HAZARDOUS_SEAS = 60;
public static final int ALERT_TYPE_HAZARDOUS_SEAS = 61;
public static final int ALERT_TYPE_SMALL_CRAFT = 62;
public static final int ALERT_TYPE_SMALL_CRAFT_WINDS = 63;
public static final int ALERT_TYPE_SMALL_CRAFT_ROUGH_BAR = 64;
public static final int ALERT_TYPE_HIGH_WATER_LEVEL = 65;
public static final int ALERT_TYPE_ASHFALL = 66;
public static final int ALERT_TYPE_FREEZING_FOG = 67;
public static final int ALERT_TYPE_DENSE_FOG = 68;
public static final int ALERT_TYPE_DENSE_SMOKE = 69;
public static final int ALERT_TYPE_BLOWING_DUST = 70;
public static final int ALERT_TYPE_HARD_FREEZE = 71;
public static final int ALERT_TYPE_FREEZE = 72;
public static final int ALERT_TYPE_FROST = 73;
public static final int ALERT_TYPE_FIRE_WEATHER = 74;
public static final int ALERT_TYPE_FLOOD = 75;
public static final int ALERT_TYPE_RIP_TIDE = 76;
public static final int ALERT_TYPE_HIGH_SURF = 77;
public static final int ALERT_TYPE_SMOG = 78;
public static final int ALERT_TYPE_AIR_QUALITY = 79;
public static final int ALERT_TYPE_BRISK_WIND = 80;
public static final int ALERT_TYPE_AIR_STAGNATION = 81;
public static final int ALERT_TYPE_LOW_WATER = 82;
public static final int ALERT_TYPE_HYDROLOGICAL = 83;
public static final int ALERT_TYPE_SPECIAL_WEATHER = 84;
public static int openWeatherCodeToFitWeatherStatus(int openWeatherCode) {
switch (openWeatherCode) {
case 800:
return CLEAR;
case 801:
case 802:
return PARTLY_CLOUDY;
case 803:
return MOSTLY_CLOUDY;
case 804:
return CLOUDY;
case 701:
case 721:
return HAZY;
case 741:
return FOG;
case 771:
case 781:
return WINDY;
case 615:
return LIGHT_RAIN_SNOW;
case 616:
return HEAVY_RAIN_SNOW;
case 611:
case 612:
case 613:
return WINTRY_MIX;
case 500:
case 520:
case 521:
case 300:
case 310:
case 313:
return LIGHT_RAIN;
case 501:
case 531:
case 301:
case 311:
return RAIN;
case 502:
case 503:
case 504:
case 522:
case 302:
case 312:
case 314:
return HEAVY_RAIN;
case 321:
return SCATTERED_SHOWERS;
case 511:
return UNKNOWN_PRECIPITATION;
case 200:
case 201:
case 202:
case 210:
case 211:
case 212:
case 230:
case 231:
case 232:
return THUNDERSTORMS;
case 221:
return SCATTERED_THUNDERSTORMS;
case 600:
return LIGHT_SNOW;
case 601:
return SNOW;
case 602:
return HEAVY_SNOW;
default:
throw new IllegalArgumentException("Unknown weather code " + openWeatherCode);
}
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class AuthNegotiationMessage {
public static final int LONG_TERM_KEY_AVAILABILITY_NONE = 0;
public static final int LONG_TERM_KEY_AVAILABILITY_SOME_AVAILABLE = 1;
public static final int ENCRYPTION_ALGORITHM_NONE = 0;
public static final int ENCRYPTION_ALGORITHM_XXTEA = 1 << 0;
public static final int ENCRYPTION_ALGORITHM_AES128 = 1 << 1;
public final byte[] packet;
public AuthNegotiationMessage(int longTermKeyAvailability, int supportedEncryptionAlgorithms) {
final MessageWriter writer = new MessageWriter(11);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_AUTH_NEGOTIATION);
writer.writeByte(longTermKeyAvailability);
writer.writeInt(supportedEncryptionAlgorithms);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,26 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class AuthNegotiationResponseMessage {
public final int status;
public final int response;
public final int longTermKeyAvailability;
public final int supportedEncryptionAlgorithms;
public AuthNegotiationResponseMessage(int status, int response, int longTermKeyAvailability, int supportedEncryptionAlgorithms) {
this.status = status;
this.response = response;
this.longTermKeyAvailability = longTermKeyAvailability;
this.supportedEncryptionAlgorithms = supportedEncryptionAlgorithms;
}
public static AuthNegotiationResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int status = reader.readByte();
final int response = reader.readByte();
final int longTermKeyAvailability = reader.readByte();
final int supportedEncryptionAlgorithms = reader.readInt();
return new AuthNegotiationResponseMessage(status, response, longTermKeyAvailability, supportedEncryptionAlgorithms);
}
}

View File

@ -0,0 +1,23 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class BatteryStatusMessage {
public final byte[] packet;
public BatteryStatusMessage(int batteryPercentage) {
final MessageWriter writer = new MessageWriter(9);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_BATTERY_STATUS);
writer.writeByte(255);
writer.writeByte(batteryPercentage);
writer.writeByte(255);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,34 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import java.util.Arrays;
public class ConfigurationMessage {
public final byte[] packet;
public final byte[] configurationPayload;
public ConfigurationMessage(byte[] configurationPayload) {
if (configurationPayload.length > 255) throw new IllegalArgumentException("Too long payload");
this.configurationPayload = configurationPayload;
final MessageWriter writer = new MessageWriter(7 + configurationPayload.length);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_CONFIGURATION);
writer.writeByte(configurationPayload.length);
writer.writeBytes(configurationPayload);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
public static ConfigurationMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int payloadSize = reader.readByte();
return new ConfigurationMessage(Arrays.copyOfRange(packet, 5, payloadSize));
}
}

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class CreateFileRequestMessage {
public final byte[] packet;
public CreateFileRequestMessage(int fileSize, int dataType, int subType, int fileIdentifier, int subTypeMask, int numberMask, String path) {
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_CREATE_FILE_REQUEST);
writer.writeInt(fileSize);
writer.writeByte(dataType);
writer.writeByte(subType);
writer.writeShort(fileIdentifier);
writer.writeByte(0); // reserved
writer.writeByte(subTypeMask);
writer.writeShort(numberMask);
writer.writeString(path);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,38 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class CreateFileResponseMessage {
public static final byte RESPONSE_FILE_CREATED_SUCCESSFULLY = 0;
public static final byte RESPONSE_FILE_ALREADY_EXISTS = 1;
public static final byte RESPONSE_NOT_ENOUGH_SPACE = 2;
public static final byte RESPONSE_NOT_SUPPORTED = 3;
public static final byte RESPONSE_NO_SLOTS_AVAILABLE_FOR_FILE_TYPE = 4;
public static final byte RESPONSE_NOT_ENOUGH_SPACE_FOR_FILE_TYPE = 5;
public final int status;
public final int response;
public final int fileIndex;
public final int dataType;
public final int subType;
public final int fileNumber;
public CreateFileResponseMessage(int status, int response, int fileIndex, int dataType, int subType, int fileNumber) {
this.status = status;
this.response = response;
this.fileIndex = fileIndex;
this.dataType = dataType;
this.subType = subType;
this.fileNumber = fileNumber;
}
public static CreateFileResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 6);
final int status = reader.readByte();
final int response = reader.readByte();
final int fileIndex = reader.readShort();
final int dataType = reader.readByte();
final int subType = reader.readByte();
final int fileNumber = reader.readShort();
return new CreateFileResponseMessage(status, response, fileIndex, dataType, subType, fileNumber);
}
}

View File

@ -0,0 +1,16 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class CurrentTimeRequestMessage {
public final int referenceID;
public CurrentTimeRequestMessage(int referenceID) {
this.referenceID = referenceID;
}
public static CurrentTimeRequestMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int referenceID = reader.readInt();
return new CurrentTimeRequestMessage(referenceID);
}
}

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class CurrentTimeRequestResponseMessage {
public final byte[] packet;
public CurrentTimeRequestResponseMessage(int status, int referenceID, int garminTimestamp, int timeZoneOffset, int dstOffset) {
final MessageWriter writer = new MessageWriter(29);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_RESPONSE);
writer.writeShort(VivomoveConstants.MESSAGE_CURRENT_TIME_REQUEST);
writer.writeByte(status);
writer.writeInt(referenceID);
writer.writeInt(garminTimestamp);
writer.writeInt(timeZoneOffset);
// TODO: next DST start/end
writer.writeInt(0);
writer.writeInt(0);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,46 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import java.util.Locale;
public class DeviceInformationMessage {
public final int protocolVersion;
public final int productNumber;
public final String unitNumber;
public final int softwareVersion;
public final int maxPacketSize;
public final String bluetoothFriendlyName;
public final String deviceName;
public final String deviceModel;
// dual-pairing flags & MAC addresses...
public DeviceInformationMessage(int protocolVersion, int productNumber, String unitNumber, int softwareVersion, int maxPacketSize, String bluetoothFriendlyName, String deviceName, String deviceModel) {
this.protocolVersion = protocolVersion;
this.productNumber = productNumber;
this.unitNumber = unitNumber;
this.softwareVersion = softwareVersion;
this.maxPacketSize = maxPacketSize;
this.bluetoothFriendlyName = bluetoothFriendlyName;
this.deviceName = deviceName;
this.deviceModel = deviceModel;
}
public static DeviceInformationMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int protocolVersion = reader.readShort();
final int productNumber = reader.readShort();
final String unitNumber = Long.toString(reader.readInt() & 0xFFFFFFFFL);
final int softwareVersion = reader.readShort();
final int maxPacketSize = reader.readShort();
final String bluetoothFriendlyName = reader.readString();
final String deviceName = reader.readString();
final String deviceModel = reader.readString();
return new DeviceInformationMessage(protocolVersion, productNumber, unitNumber, softwareVersion, maxPacketSize, bluetoothFriendlyName, deviceName, deviceModel);
}
public String getSoftwareVersionStr() {
int softwareVersionMajor = softwareVersion / 100;
int softwareVersionMinor = softwareVersion % 100;
return String.format(Locale.ROOT, "%d.%02d", softwareVersionMajor, softwareVersionMinor);
}
}

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class DeviceInformationResponseMessage {
public final byte[] packet;
public DeviceInformationResponseMessage(int status, int protocolVersion, int productNumber, int unitNumber, int softwareVersion, int maxPacketSize, String bluetoothFriendlyName, String deviceName, String deviceModel, int protocolFlags) {
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_RESPONSE);
writer.writeShort(VivomoveConstants.MESSAGE_DEVICE_INFORMATION);
writer.writeByte(status);
writer.writeShort(protocolVersion);
writer.writeShort(productNumber);
writer.writeInt(unitNumber);
writer.writeShort(softwareVersion);
writer.writeShort(maxPacketSize);
writer.writeString(bluetoothFriendlyName);
writer.writeString(deviceName);
writer.writeString(deviceModel);
writer.writeByte(protocolFlags);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,26 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class DirectoryFileFilterRequestMessage {
public static final int FILTER_NO_FILTER = 0;
public static final int FILTER_DEVICE_DEFAULT_FILTER = 1;
public static final int FILTER_CUSTOM_FILTER = 2;
public static final int FILTER_PENDING_UPLOADS_ONLY = 3;
public final byte[] packet;
public DirectoryFileFilterRequestMessage(int filterType) {
final MessageWriter writer = new MessageWriter(7);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_DIRECTORY_FILE_FILTER_REQUEST);
writer.writeByte(filterType);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,23 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class DirectoryFileFilterResponseMessage {
public final int status;
public final int response;
public static final int RESPONSE_DIRECTORY_FILTER_APPLIED = 0;
public static final int RESPONSE_FAILED_TO_APPLY_DIRECTORY_FILTER = 1;
public DirectoryFileFilterResponseMessage(int status, int response) {
this.status = status;
this.response = response;
}
public static DirectoryFileFilterResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int status = reader.readByte();
final int response = reader.readByte();
return new DirectoryFileFilterResponseMessage(status, response);
}
}

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class DownloadRequestMessage {
public static final int REQUEST_CONTINUE_TRANSFER = 0;
public static final int REQUEST_NEW_TRANSFER = 1;
public final byte[] packet;
public DownloadRequestMessage(int fileIndex, int dataOffset, int request, int crcSeed, int dataSize) {
final MessageWriter writer = new MessageWriter(19);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_DOWNLOAD_REQUEST);
writer.writeShort(fileIndex);
writer.writeInt(dataOffset);
writer.writeByte(request);
writer.writeShort(crcSeed);
writer.writeInt(dataSize);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class DownloadRequestResponseMessage {
public final int status;
public final int response;
public final int fileSize;
public static final byte RESPONSE_DOWNLOAD_REQUEST_OKAY = 0;
public static final byte RESPONSE_DATA_DOES_NOT_EXIST = 1;
public static final byte RESPONSE_DATA_EXISTS_BUT_IS_NOT_DOWNLOADABLE = 2;
public static final byte RESPONSE_NOT_READY_TO_DOWNLOAD = 3;
public static final byte RESPONSE_REQUEST_INVALID = 4;
public static final byte RESPONSE_CRC_INCORRECT = 5;
public static final byte RESPONSE_DATA_REQUESTED_EXCEEDS_FILE_SIZE = 6;
public DownloadRequestResponseMessage(int status, int response, int fileSize) {
this.status = status;
this.response = response;
this.fileSize = fileSize;
}
public static DownloadRequestResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int status = reader.readByte();
final int response = reader.readByte();
final int fileSize = reader.readInt();
return new DownloadRequestResponseMessage(status, response, fileSize);
}
}

View File

@ -0,0 +1,44 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class FileReadyMessage {
public static final int TRIGGER_MANUAL = 0;
public static final int TRIGGER_AUTOMATIC = 1;
public final int fileIndex;
public final int dataType;
public final int fileSubtype;
public final int fileNumber;
public final int specificFileFlags;
public final int generalFileFlags;
public final int fileSize;
public final int fileDate;
public final int triggerMethod;
public FileReadyMessage(int fileIndex, int dataType, int fileSubtype, int fileNumber, int specificFileFlags, int generalFileFlags, int fileSize, int fileDate, int triggerMethod) {
this.fileIndex = fileIndex;
this.dataType = dataType;
this.fileSubtype = fileSubtype;
this.fileNumber = fileNumber;
this.specificFileFlags = specificFileFlags;
this.generalFileFlags = generalFileFlags;
this.fileSize = fileSize;
this.fileDate = fileDate;
this.triggerMethod = triggerMethod;
}
public static FileReadyMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int fileIndex = reader.readShort();
final int dataType = reader.readByte();
final int fileSubtype = reader.readByte();
final int fileNumber = reader.readShort();
final int specificFileFlags = reader.readByte();
final int generalFileFlags = reader.readByte();
final int fileSize = reader.readInt();
final int fileDate = reader.readInt();
final int triggerMethod = reader.readByte();
return new FileReadyMessage(fileIndex, dataType, fileSubtype, fileNumber, specificFileFlags, generalFileFlags, fileSize, fileDate, triggerMethod);
}
}

View File

@ -0,0 +1,46 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class FileTransferDataMessage {
public final int flags;
public final int crc;
public final int dataOffset;
public final byte[] data;
public final byte[] packet;
public FileTransferDataMessage(int flags, int crc, int dataOffset, byte[] data) {
this.flags = flags;
this.crc = crc;
this.dataOffset = dataOffset;
this.data = data;
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_FILE_TRANSFER_DATA);
writer.writeByte(flags);
writer.writeShort(crc);
writer.writeInt(dataOffset);
writer.writeBytes(data);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
public static FileTransferDataMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int flags = reader.readByte();
final int crc = reader.readShort();
final int dataOffset = reader.readInt();
final int dataSize = packet.length - 13;
final byte[] data = reader.readBytes(dataSize);
return new FileTransferDataMessage(flags, crc, dataOffset, data);
}
}

View File

@ -0,0 +1,48 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class FileTransferDataResponseMessage {
public static final byte RESPONSE_TRANSFER_SUCCESSFUL = 0;
public static final byte RESPONSE_RESEND_LAST_DATA_PACKET = 1;
public static final byte RESPONSE_ABORT_DOWNLOAD_REQUEST = 2;
public static final byte RESPONSE_ERROR_CRC_MISMATCH = 3;
public static final byte RESPONSE_ERROR_DATA_OFFSET_MISMATCH = 4;
public static final byte RESPONSE_SILENT_SYNC_PAUSED = 5;
public final int status;
public final int response;
public final int nextDataOffset;
public final byte[] packet;
public FileTransferDataResponseMessage(int status, int response, int nextDataOffset) {
this.status = status;
this.response = response;
this.nextDataOffset = nextDataOffset;
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_RESPONSE);
writer.writeShort(VivomoveConstants.MESSAGE_FILE_TRANSFER_DATA);
writer.writeByte(status);
writer.writeByte(response);
writer.writeInt(nextDataOffset);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
public static FileTransferDataResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 6);
final int status = reader.readByte();
final int response = reader.readByte();
final int nextDataOffset = reader.readInt();
return new FileTransferDataResponseMessage(status, response, nextDataOffset);
}
}

View File

@ -0,0 +1,17 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class FindMyPhoneRequestMessage {
public final int duration;
public FindMyPhoneRequestMessage(int duration) {
this.duration = duration;
}
public static FindMyPhoneRequestMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int duration = reader.readByte();
return new FindMyPhoneRequestMessage(duration);
}
}

View File

@ -0,0 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit.FitMessage;
public class FitDataMessage {
public final byte[] packet;
public FitDataMessage(FitMessage... messages) {
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_FIT_DATA);
for (FitMessage message : messages) {
message.writeToMessage(writer);
}
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,27 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class FitDataResponseMessage {
public final int requestID;
public final int status;
public final int fitResponse;
public FitDataResponseMessage(int requestID, int status, int fitResponse) {
this.requestID = requestID;
this.status = status;
this.fitResponse = fitResponse;
}
public static FitDataResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int status = reader.readByte();
final int fitResponse = reader.readByte();
return new FitDataResponseMessage(requestID, status, fitResponse);
}
public static final int RESPONSE_APPLIED = 0;
public static final int RESPONSE_NO_DEFINITION = 1;
public static final int RESPONSE_MISMATCH = 2;
public static final int RESPONSE_NOT_READY = 3;
}

View File

@ -0,0 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit.FitMessageDefinition;
public class FitDefinitionMessage {
public final byte[] packet;
public FitDefinitionMessage(FitMessageDefinition... definitions) {
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_FIT_DEFINITION);
for (FitMessageDefinition definition : definitions) {
definition.writeToMessage(writer);
}
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,27 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class FitDefinitionResponseMessage {
public final int requestID;
public final int status;
public final int fitResponse;
public FitDefinitionResponseMessage(int requestID, int status, int fitResponse) {
this.requestID = requestID;
this.status = status;
this.fitResponse = fitResponse;
}
public static FitDefinitionResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int status = reader.readByte();
final int fitResponse = reader.readByte();
return new FitDefinitionResponseMessage(requestID, status, fitResponse);
}
public static final int RESPONSE_APPLIED = 0;
public static final int RESPONSE_NOT_UNIQUE = 1;
public static final int RESPONSE_OUT_OF_RANGE = 2;
public static final int RESPONSE_NOT_READY = 3;
}

View File

@ -0,0 +1,22 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class GenericResponseMessage {
public final byte[] packet;
public GenericResponseMessage(int originalRequestID, int status) {
final MessageWriter writer = new MessageWriter(9);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_RESPONSE);
writer.writeShort(originalRequestID);
writer.writeByte(status);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,17 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.AncsControlCommand;
public class GncsControlPointMessage {
public final AncsControlCommand command;
public GncsControlPointMessage(AncsControlCommand command) {
this.command = command;
}
public static GncsControlPointMessage parsePacket(byte[] packet) {
final AncsControlCommand command = AncsControlCommand.parseCommand(packet, 4, packet.length - 6);
if (command == null) return null;
return new GncsControlPointMessage(command);
}
}

View File

@ -0,0 +1,34 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class GncsControlPointResponseMessage {
public static final int RESPONSE_SUCCESSFUL = 0;
public static final int RESPONSE_ANCS_ERROR_OCCURRED = 1;
public static final int RESPONSE_INVALID_PARAMETERS = 2;
public static final int ANCS_ERROR_NO_ERROR = 0;
public static final int ANCS_ERROR_UNKNOWN_ANCS_COMMAND = 0xA0;
public static final int ANCS_ERROR_INVALID_ANCS_COMMAND = 0xA1;
public static final int ANCS_ERROR_INVALID_ANCS_PARAMETER = 0xA2;
public static final int ANCS_ERROR_ACTION_FAILED = 0xA3;
public final byte[] packet;
public GncsControlPointResponseMessage(int status, int response, int ancsError) {
final MessageWriter writer = new MessageWriter(11);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_RESPONSE);
writer.writeShort(VivomoveConstants.MESSAGE_GNCS_CONTROL_POINT_REQUEST);
writer.writeByte(status);
writer.writeByte(response);
writer.writeByte(ancsError);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,25 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class GncsDataSourceMessage {
public final byte[] packet;
public GncsDataSourceMessage(byte[] ancsMessage, int dataOffset, int size) {
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_GNCS_DATA_SOURCE);
writer.writeShort(ancsMessage.length);
// TODO: CRC Seed!
writer.writeShort(ChecksumCalculator.computeCrc(ancsMessage, dataOffset, size));
writer.writeShort(dataOffset);
writer.writeBytes(ancsMessage, dataOffset, size);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,26 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class GncsDataSourceResponseMessage {
public static final int RESPONSE_TRANSFER_SUCCESSFUL = 0;
public static final int RESPONSE_RESEND_LAST_DATA_PACKET = 1;
public static final int RESPONSE_ABORT_REQUEST = 2;
public static final int RESPONSE_ERROR_CRC_MISMATCH = 3;
public static final int RESPONSE_ERROR_DATA_OFFSET_MISMATCH = 4;
public final int status;
public final int response;
public GncsDataSourceResponseMessage(int status, int response) {
this.status = status;
this.response = response;
}
public static GncsDataSourceResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestMessageID = reader.readShort();
final int status = reader.readByte();
final int response = reader.readByte();
return new GncsDataSourceResponseMessage(status, response);
}
}

View File

@ -0,0 +1,35 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.AncsCategory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.AncsEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.AncsEventFlag;
import org.apache.commons.lang3.EnumUtils;
import java.util.Set;
public class GncsNotificationSourceMessage {
public final byte[] packet;
public GncsNotificationSourceMessage(AncsEvent event, Set<AncsEventFlag> eventFlags, AncsCategory category, int categoryCount, int notificationUID) {
final MessageWriter writer = new MessageWriter(15);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_NOTIFICATION_SOURCE);
writer.writeByte(event.ordinal());
writer.writeByte(eventFlags == null ? 26 : ((int) EnumUtils.generateBitVector(AncsEventFlag.class, eventFlags)));
writer.writeByte(category.ordinal());
writer.writeByte(Math.min(categoryCount, 127));
writer.writeInt(notificationUID);
// TODO: Extra flags?
writer.writeByte(3);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,84 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import java.nio.charset.StandardCharsets;
public class MessageReader {
private final byte[] data;
private int position;
public MessageReader(byte[] data) {
this.data = data;
}
public MessageReader(byte[] data, int skipOffset) {
this.data = data;
this.position = skipOffset;
}
public boolean isEof() {
return position >= data.length;
}
public int getPosition() {
return position;
}
public void skip(int offset) {
if (position + offset > data.length) throw new IllegalStateException();
position += offset;
}
public int readByte() {
if (position + 1 > data.length) throw new IllegalStateException();
final int result = BinaryUtils.readByte(data, position);
++position;
return result;
}
public int readShort() {
if (position + 2 > data.length) throw new IllegalStateException();
final int result = BinaryUtils.readShort(data, position);
position += 2;
return result;
}
public int readInt() {
if (position + 4 > data.length) throw new IllegalStateException();
final int result = BinaryUtils.readInt(data, position);
position += 4;
return result;
}
public long readLong() {
if (position + 8 > data.length) throw new IllegalStateException();
final long result = BinaryUtils.readLong(data, position);
position += 8;
return result;
}
public String readString() {
final int size = readByte();
if (position + size > data.length) throw new IllegalStateException();
final String result = new String(data, position, size, StandardCharsets.UTF_8);
position += size;
return result;
}
public byte[] readBytes(int size) {
if (position + size > data.length) throw new IllegalStateException();
final byte[] result = new byte[size];
System.arraycopy(data, position, result, 0, size);
position += size;
return result;
}
public byte[] readBytesTo(int size, byte[] buffer, int offset) {
if (offset + size > buffer.length) throw new IllegalArgumentException();
if (position + size > data.length) throw new IllegalStateException();
System.arraycopy(data, position, buffer, offset, size);
position += size;
return buffer;
}
}

View File

@ -0,0 +1,77 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class MessageWriter {
private static final int DEFAULT_BUFFER_SIZE = 16384;
private final byte[] buffer;
private int position;
public MessageWriter() {
this(DEFAULT_BUFFER_SIZE);
}
public MessageWriter(int bufferSize) {
this.buffer = new byte[bufferSize];
}
public void writeByte(int value) {
if (position + 1 > buffer.length) throw new IllegalStateException();
BinaryUtils.writeByte(buffer, position, value);
++position;
}
public void writeShort(int value) {
if (position + 2 > buffer.length) throw new IllegalStateException();
BinaryUtils.writeShort(buffer, position, value);
position += 2;
}
public void writeInt(int value) {
if (position + 4 > buffer.length) throw new IllegalStateException();
BinaryUtils.writeInt(buffer, position, value);
position += 4;
}
public void writeLong(long value) {
if (position + 8 > buffer.length) throw new IllegalStateException();
BinaryUtils.writeLong(buffer, position, value);
position += 8;
}
public void writeString(String value) {
final byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
final int size = bytes.length;
if (size > 255) throw new IllegalArgumentException("Too long string");
if (position + 1 + size > buffer.length) throw new IllegalStateException();
writeByte(size);
System.arraycopy(bytes, 0, buffer, position, size);
position += size;
}
public byte[] getBytes() {
return position == buffer.length ? buffer : Arrays.copyOf(buffer, position);
}
public byte[] peekBytes() {
return buffer;
}
public int getSize() {
return position;
}
public void writeBytes(byte[] bytes) {
writeBytes(bytes, 0, bytes.length);
}
public void writeBytes(byte[] bytes, int offset, int size) {
if (position + size > buffer.length) throw new IllegalStateException();
System.arraycopy(bytes, offset, buffer, position, size);
position += size;
}
}

View File

@ -0,0 +1,16 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class MusicControlCapabilitiesMessage {
public final int supportedCapabilities;
public MusicControlCapabilitiesMessage(int supportedCapabilities) {
this.supportedCapabilities = supportedCapabilities;
}
public static MusicControlCapabilitiesMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int supportedCapabilities = reader.readByte();
return new MusicControlCapabilitiesMessage(supportedCapabilities);
}
}

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminMusicControlCommand;
public class MusicControlCapabilitiesResponseMessage {
public final byte[] packet;
public MusicControlCapabilitiesResponseMessage(int status, GarminMusicControlCommand[] commands) {
if (commands.length > 255) throw new IllegalArgumentException("Too many supported commands");
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_RESPONSE);
writer.writeShort(VivomoveConstants.MESSAGE_MUSIC_CONTROL_CAPABILITIES);
writer.writeByte(status);
writer.writeByte(commands.length);
for (GarminMusicControlCommand command : commands) {
writer.writeByte(command.ordinal());
}
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ams.AmsEntityAttribute;
public class MusicControlEntityUpdateMessage {
public final byte[] packet;
public MusicControlEntityUpdateMessage(AmsEntityAttribute... attributes) {
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_MUSIC_CONTROL_ENTITY_UPDATE);
for (AmsEntityAttribute attribute : attributes) {
attribute.writeToMessage(writer);
}
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,144 @@
Generic message structure:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size | Message ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| Payload... |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | CRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Generic response message (MESSAGE_RESPONSE) structure:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size | Message ID=5000 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Request Message ID | Status | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| |
+ +
| Payload... |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | CRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
MESSAGE_DEVICE_INFORMATION:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size | Message ID=5024 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Protocol Version | Product Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unit Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Software Version | Max Packet Size |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Bluetooth Name.| |
+-+-+-+-+-+-+-+-+ +
| Bluetooth Name UTF-8... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Device Name Le.| |
+-+-+-+-+-+-+-+-+ +
| Device Name UTF-8... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Device Model L.| |
+-+-+-+-+-+-+-+-+ +
| Device Model UTF-8... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| CRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
MESSAGE_BATTERY_STATUS:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size | Message ID=5023 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|unknown| OK|un.| Voltage | Capacity | Remaining Time|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| CRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
MESSAGE_DEVICE_SETTINGS:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size | Message ID=5026 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Settings Count| Setting Type | Setting Size | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| Setting Bytes... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Setting Type 2| Setting Size 2| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| Setting Bytes 2... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| CRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
MESSAGE_PROTOBUF_REQUEST:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size | Message ID=5043 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Request ID | Data Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | Total Protobuf Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | Protobuf Data Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| Protobuf Payload... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| CRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
MESSAGE_PROTOBUF_REQUEST response:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size | Message ID=5000 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Request Message ID=5043 | Status | Request ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | Data Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | Status | Error | CRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+-+-+-+-+-+-+-+-+
MESSAGE_CONFIGURATION:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size | Message ID=5050 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Bitflags Length| Capability Bitflags... |
+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | CRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

View File

@ -0,0 +1,32 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class NotificationServiceSubscriptionMessage {
public static final int INTENT_UNSUBSCRIBE = 0;
public static final int INTENT_SUBSCRIBE = 1;
private static final int FEATURE_FLAG_PHONE_NUMBER = 1;
public final int intentIndicator;
public final int featureFlags;
public NotificationServiceSubscriptionMessage(int intentIndicator, int featureFlags) {
this.intentIndicator = intentIndicator;
this.featureFlags = featureFlags;
}
public boolean isSubscribe() {
return intentIndicator == INTENT_SUBSCRIBE;
}
public boolean hasPhoneNumberSupport() {
return (featureFlags & FEATURE_FLAG_PHONE_NUMBER) != 0;
}
public static NotificationServiceSubscriptionMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int intentIndicator = reader.readByte();
final int featureFlags = packet.length > 7 ? reader.readByte() : 0;
return new NotificationServiceSubscriptionMessage(intentIndicator, featureFlags);
}
}

View File

@ -0,0 +1,25 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class NotificationServiceSubscriptionResponseMessage {
public final byte[] packet;
public NotificationServiceSubscriptionResponseMessage(int status, int response, int intent, int featureFlags) {
final MessageWriter writer = new MessageWriter(12);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_RESPONSE);
writer.writeShort(VivomoveConstants.MESSAGE_NOTIFICATION_SERVICE_SUBSCRIPTION);
writer.writeByte(status);
writer.writeByte(response);
writer.writeByte(intent);
writer.writeByte(featureFlags);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,46 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class ProtobufRequestMessage {
public final byte[] packet;
public final int requestId;
public final int dataOffset;
public final int totalProtobufLength;
public final int protobufDataLength;
public final byte[] messageBytes;
public ProtobufRequestMessage(int requestId, int dataOffset, int totalProtobufLength, int protobufDataLength, byte[] messageBytes) {
this.requestId = requestId;
this.dataOffset = dataOffset;
this.totalProtobufLength = totalProtobufLength;
this.protobufDataLength = protobufDataLength;
this.messageBytes = messageBytes;
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_PROTOBUF_REQUEST);
writer.writeShort(requestId);
writer.writeInt(dataOffset);
writer.writeInt(totalProtobufLength);
writer.writeInt(protobufDataLength);
writer.writeBytes(messageBytes);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
public static ProtobufRequestMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int dataOffset = reader.readInt();
final int totalProtobufLength= reader.readInt();
final int protobufDataLength = reader.readInt();
final byte[] messageBytes = reader.readBytes(protobufDataLength);
return new ProtobufRequestMessage(requestID, dataOffset, totalProtobufLength, protobufDataLength, messageBytes);
}
}

View File

@ -0,0 +1,37 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class ProtobufRequestResponseMessage {
public static final int NO_ERROR = 0;
public static final int UNKNOWN_REQUEST_ID = 100;
public static final int DUPLICATE_PACKET = 101;
public static final int MISSING_PACKET = 102;
public static final int EXCEEDED_TOTAL_PROTOBUF_LENGTH = 103;
public static final int PROTOBUF_PARSE_ERROR = 200;
public static final int UNKNOWN_PROTOBUF_MESSAGE = 201;
public final int status;
public final int requestId;
public final int dataOffset;
public final int protobufStatus;
public final int error;
public ProtobufRequestResponseMessage(int status, int requestId, int dataOffset, int protobufStatus, int error) {
this.status = status;
this.requestId = requestId;
this.dataOffset = dataOffset;
this.protobufStatus = protobufStatus;
this.error = error;
}
public static ProtobufRequestResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestMessageID = reader.readShort();
final int status = reader.readByte();
final int requestID = reader.readShort();
final int dataOffset = reader.readInt();
final int protobufStatus = reader.readByte();
final int error = reader.readByte();
return new ProtobufRequestResponseMessage(status, requestID, dataOffset, protobufStatus, error);
}
}

View File

@ -0,0 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminDeviceSetting;
import java.util.Map;
public class RequestMessage {
public final byte[] packet;
public RequestMessage(int requestMessageID) {
final MessageWriter writer = new MessageWriter(8);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_REQUEST);
writer.writeShort(requestMessageID);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,42 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import java.util.Locale;
public class ResponseMessage {
public final int requestID;
public final int status;
public ResponseMessage(int requestID, int status) {
this.requestID = requestID;
this.status = status;
}
public static ResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int status = reader.readByte();
return new ResponseMessage(requestID, status);
}
public String getStatusStr() {
switch (status) {
case VivomoveConstants.STATUS_ACK:
return "ACK";
case VivomoveConstants.STATUS_NAK:
return "NAK";
case VivomoveConstants.STATUS_UNSUPPORTED:
return "UNSUPPORTED";
case VivomoveConstants.STATUS_DECODE_ERROR:
return "DECODE ERROR";
case VivomoveConstants.STATUS_CRC_ERROR:
return "CRC ERROR";
case VivomoveConstants.STATUS_LENGTH_ERROR:
return "LENGTH ERROR";
default:
return String.format(Locale.ROOT, "Unknown status %x", status);
}
}
}

View File

@ -0,0 +1,44 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminDeviceSetting;
import java.util.Map;
public class SetDeviceSettingsMessage {
public final byte[] packet;
public SetDeviceSettingsMessage(Map<GarminDeviceSetting, Object> settings) {
final int settingsCount = settings.size();
if (settingsCount == 0) throw new IllegalArgumentException("Empty settings");
if (settingsCount > 255) throw new IllegalArgumentException("Too many settings");
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_DEVICE_SETTINGS);
writer.writeByte(settings.size());
for (Map.Entry<GarminDeviceSetting, Object> settingPair : settings.entrySet()) {
final GarminDeviceSetting setting = settingPair.getKey();
writer.writeByte(setting.ordinal());
final Object value = settingPair.getValue();
if (value instanceof String) {
writer.writeString((String) value);
} else if (value instanceof Integer) {
writer.writeByte(4);
writer.writeInt((Integer) value);
} else if (value instanceof Boolean) {
writer.writeByte(1);
writer.writeByte(Boolean.TRUE.equals(value) ? 1 : 0);
} else {
throw new IllegalArgumentException("Unsupported setting value type " + value);
}
}
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,20 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class SetDeviceSettingsResponseMessage {
public final int status;
public final int response;
public SetDeviceSettingsResponseMessage(int status, int response) {
this.status = status;
this.response = response;
}
public static SetDeviceSettingsResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int status = reader.readByte();
final int response = reader.readByte();
return new SetDeviceSettingsResponseMessage(status, response);
}
}

View File

@ -0,0 +1,20 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class SupportedFileTypesRequestMessage {
public final byte[] packet;
public SupportedFileTypesRequestMessage() {
final MessageWriter writer = new MessageWriter(6);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_SUPPORTED_FILE_TYPES_REQUEST);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,48 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import java.util.ArrayList;
import java.util.List;
public class SupportedFileTypesResponseMessage {
public static final int FILE_DATA_TYPE_FIT = 128;
public static final int FILE_DATA_TYPE_GRAPHIC = 2;
public static final int FILE_DATA_TYPE_INVALID = -1;
public static final int FILE_DATA_TYPE_NON_FIT = 255;
public final int status;
public final List<FileTypeInfo> fileTypes;
public SupportedFileTypesResponseMessage(int status, List<FileTypeInfo> fileTypes) {
this.status = status;
this.fileTypes = fileTypes;
}
public static SupportedFileTypesResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int status = reader.readByte();
final int typeCount = reader.readByte();
final List<FileTypeInfo> types = new ArrayList<>(typeCount);
for (int i = 0; i < typeCount; ++i) {
final int fileDataType = reader.readByte();
final int fileSubType = reader.readByte();
final String garminDeviceFileType = reader.readString();
types.add(new FileTypeInfo(fileDataType, fileSubType, garminDeviceFileType));
}
return new SupportedFileTypesResponseMessage(status, types);
}
public static class FileTypeInfo {
public final int fileDataType;
public final int fileSubType;
public final String garminDeviceFileType;
public FileTypeInfo(int fileDataType, int fileSubType, String garminDeviceFileType) {
this.fileDataType = fileDataType;
this.fileSubType = fileSubType;
this.garminDeviceFileType = garminDeviceFileType;
}
}
}

View File

@ -0,0 +1,45 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminMessageType;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import java.util.HashSet;
import java.util.Set;
public class SyncRequestMessage {
public static final int OPTION_MANUAL = 0;
public static final int OPTION_INVISIBLE = 1;
public static final int OPTION_VISIBLE_AS_NEEDED = 2;
public final int option;
public final Set<GarminMessageType> fileTypes;
public SyncRequestMessage(int option, Set<GarminMessageType> fileTypes) {
this.option = option;
this.fileTypes = fileTypes;
}
public static SyncRequestMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int option = reader.readByte();
final int bitMaskSize = reader.readByte();
final byte[] longBits = reader.readBytesTo(bitMaskSize, new byte[8], 0);
long bitMask = BinaryUtils.readLong(longBits, 0);
final Set<GarminMessageType> fileTypes = new HashSet<>(GarminMessageType.values().length);
for (GarminMessageType messageType : GarminMessageType.values()) {
int num = messageType.ordinal();
long mask = 1L << num;
if ((bitMask & mask) != 0) {
fileTypes.add(messageType);
bitMask &= ~mask;
}
}
if (bitMask != 0) {
throw new IllegalArgumentException("Unknown bit mask " + GB.hexdump(longBits, 0, longBits.length));
}
return new SyncRequestMessage(option, fileTypes);
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminSystemEventType;
public class SystemEventMessage {
public final byte[] packet;
public SystemEventMessage(GarminSystemEventType eventType, Object value) {
final MessageWriter writer = new MessageWriter();
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_SYSTEM_EVENT);
writer.writeByte(eventType.ordinal());
if (value instanceof String) {
writer.writeString((String) value);
} else if (value instanceof Integer) {
writer.writeByte((Integer) value);
} else {
throw new IllegalArgumentException("Unsupported event value type " + value);
}
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

View File

@ -0,0 +1,20 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
public class SystemEventResponseMessage {
public final int status;
public final int response;
public SystemEventResponseMessage(int status, int response) {
this.status = status;
this.response = response;
}
public static SystemEventResponseMessage parsePacket(byte[] packet) {
final MessageReader reader = new MessageReader(packet, 4);
final int requestID = reader.readShort();
final int status = reader.readByte();
final int response = reader.readByte();
return new SystemEventResponseMessage(status, response);
}
}

View File

@ -0,0 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
public class UploadRequestMessage {
public final byte[] packet;
public UploadRequestMessage(int fileIndex, int dataOffset, int maxSize, int crcSeed) {
final MessageWriter writer = new MessageWriter(18);
writer.writeShort(0); // packet size will be filled below
writer.writeShort(VivomoveConstants.MESSAGE_UPLOAD_REQUEST);
writer.writeShort(fileIndex);
writer.writeInt(maxSize);
writer.writeInt(dataOffset);
writer.writeShort(crcSeed);
writer.writeShort(0); // CRC will be filled below
final byte[] packet = writer.getBytes();
BinaryUtils.writeShort(packet, 0, packet.length);
BinaryUtils.writeShort(packet, packet.length - 2, ChecksumCalculator.computeCrc(packet, 0, packet.length - 2));
this.packet = packet;
}
}

Some files were not shown because too many files have changed in this diff Show More