mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-27 04:16:49 +01:00
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:
parent
114f6fcbf0
commit
3a58314db6
@ -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();
|
||||
|
@ -63,7 +63,7 @@ public class VivomoveHrCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount() {
|
||||
public int getAlarmSlotCount(GBDevice device) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ams;
|
||||
|
||||
public enum AmsEntity {
|
||||
PLAYER,
|
||||
QUEUE,
|
||||
TRACK
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
|
||||
|
||||
public enum AncsAction {
|
||||
POSITIVE,
|
||||
NEGATIVE
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
|
||||
|
||||
public enum AncsAppAttribute {
|
||||
DISPLAY_NAME
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
|
||||
|
||||
public enum AncsEvent {
|
||||
NOTIFICATION_ADDED,
|
||||
NOTIFICATION_MODIFIED,
|
||||
NOTIFICATION_REMOVED
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs;
|
||||
|
||||
public enum AncsEventFlag {
|
||||
SILENT,
|
||||
IMPORTANT,
|
||||
PRE_EXISTING,
|
||||
POSITIVE_ACTION,
|
||||
NEGATIVE_ACTION
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.VivomoveHrActivitySample;
|
||||
|
||||
interface FitImportProcessor {
|
||||
void onSample(VivomoveHrActivitySample sample);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user