mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-09 03:37:03 +01:00
Garmin: add gpx import functionality for models that support it
Add a Field definition for GPS coordinates and remove the corresponding method from GarminUtils. Add a new message COURSE and some fields to other known messages. Also centralize some utility methods in GpxParser and GpxTrack, adapting ZeppOsGpxRouteFile. Be aware that the capability used to identify the supported watches might be the wrong one.
This commit is contained in:
parent
f2f6536ea8
commit
29787d0c9b
@ -1,6 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@ -12,6 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
|
||||
@ -265,4 +270,14 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return getPrefs(device).getStringSet(GarminPreferences.PREF_GARMIN_CAPABILITIES, Collections.emptySet())
|
||||
.contains(capability.name());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
|
||||
final GarminGpxRouteInstallHandler garminGpxRouteInstallHandler = new GarminGpxRouteInstallHandler(uri, context);
|
||||
if (garminGpxRouteInstallHandler.isValid())
|
||||
return garminGpxRouteInstallHandler;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,139 @@
|
||||
/* Copyright (C) 2023-2024 Daniel Dakhno, José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GpxRouteFileConverter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability.COURSE_DOWNLOAD;
|
||||
|
||||
public class GarminGpxRouteInstallHandler implements InstallHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GarminGpxRouteInstallHandler.class);
|
||||
|
||||
protected final Context mContext;
|
||||
public byte[] rawBytes;
|
||||
private GpxRouteFileConverter gpxRouteFileConverter;
|
||||
|
||||
public GarminGpxRouteInstallHandler(final Uri uri, final Context context) {
|
||||
this.mContext = context;
|
||||
|
||||
final UriHelper uriHelper;
|
||||
try {
|
||||
uriHelper = UriHelper.get(uri, context);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to get uri", e);
|
||||
return;
|
||||
}
|
||||
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
|
||||
rawBytes = FileUtils.readAll(in, 1024 * 1024); // 1MB
|
||||
|
||||
final GpxRouteFileConverter gpxRouteFileConverter1 = new GpxRouteFileConverter(rawBytes);
|
||||
if (gpxRouteFileConverter1.isValid()) {
|
||||
this.gpxRouteFileConverter = gpxRouteFileConverter1;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to read file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return gpxRouteFileConverter != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateInstallation(final InstallActivity installActivity, final GBDevice device) {
|
||||
if (device.isBusy()) {
|
||||
installActivity.setInfoText(device.getBusyTask());
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
if (!(coordinator instanceof GarminCoordinator)) {
|
||||
LOG.warn("Coordinator is not a GarminCoordinator: {}", coordinator.getClass());
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
final GarminCoordinator garminCoordinator = (GarminCoordinator) coordinator;
|
||||
if (!garminCoordinator.supports(device, COURSE_DOWNLOAD)) {
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!device.isInitialized()) {
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready));
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final GenericItem fwItem = createInstallItem(device);
|
||||
fwItem.setIcon(coordinator.getDefaultIconResource());
|
||||
|
||||
if (gpxRouteFileConverter == null) {
|
||||
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version));
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwinstaller_firmware_not_compatible_to_device));
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
final String gpxRoute = mContext.getString(R.string.kind_gpx_route);
|
||||
builder.append(mContext.getString(R.string.fw_upgrade_notice, gpxRoute));
|
||||
installActivity.setInfoText(builder.toString());
|
||||
installActivity.setInstallItem(fwItem);
|
||||
installActivity.setInstallEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartInstall(final GBDevice device) {
|
||||
}
|
||||
|
||||
public GpxRouteFileConverter getGpxRouteFileConverter() {
|
||||
return gpxRouteFileConverter;
|
||||
}
|
||||
|
||||
private GenericItem createInstallItem(final GBDevice device) {
|
||||
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
final String firmwareName = mContext.getString(
|
||||
R.string.installhandler_firmware_name,
|
||||
mContext.getString(coordinator.getDeviceNameResource()),
|
||||
mContext.getString(R.string.kind_gpx_route),
|
||||
gpxRouteFileConverter.getName()
|
||||
);
|
||||
return new GenericItem(firmwareName);
|
||||
}
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Comparator;
|
||||
@ -51,6 +53,34 @@ public class GPSCoordinate {
|
||||
return altitude;
|
||||
}
|
||||
|
||||
public double getDistance(GPSCoordinate source) {
|
||||
final Location end = new Location("end");
|
||||
end.setLatitude(this.getLatitude());
|
||||
end.setLongitude(this.getLongitude());
|
||||
|
||||
final Location start = new Location("start");
|
||||
start.setLatitude(source.getLatitude());
|
||||
start.setLongitude(source.getLongitude());
|
||||
|
||||
return end.distanceTo(start);
|
||||
}
|
||||
|
||||
public double getAltitudeDifference(GPSCoordinate source) {
|
||||
if (this.getAltitude() == UNKNOWN_ALTITUDE)
|
||||
return 0;
|
||||
if (source.getAltitude() == UNKNOWN_ALTITUDE)
|
||||
return 0;
|
||||
return this.getAltitude() - source.getAltitude();
|
||||
}
|
||||
|
||||
public double getAscent(GPSCoordinate source) {
|
||||
return Math.max(0, this.getAltitudeDifference(source));
|
||||
}
|
||||
|
||||
public double getDescent(GPSCoordinate source) {
|
||||
return Math.max(0, -this.getAltitudeDifference(source));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -17,6 +17,7 @@ import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.FileDownloadedDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.CreateFileMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.DownloadRequestMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FileTransferDataMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
|
||||
@ -87,10 +88,10 @@ public class FileTransferHandler implements MessageHandler {
|
||||
// return new DownloadRequestMessage(0, 0, DownloadRequestMessage.REQUEST_TYPE.NEW, 0, 0);
|
||||
// }
|
||||
//
|
||||
// public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYPE filetype) {
|
||||
// upload.setCurrentlyUploading(new FileFragment(new DirectoryEntry(0, filetype, 0, 0, 0, fileAsByteArray.length, null), fileAsByteArray));
|
||||
// return new CreateFileMessage(fileAsByteArray.length, filetype);
|
||||
// }
|
||||
public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYPE filetype) {
|
||||
upload.setCurrentlyUploading(new FileFragment(new DirectoryEntry(0, filetype, 0, 0, 0, fileAsByteArray.length, null), fileAsByteArray));
|
||||
return new CreateFileMessage(fileAsByteArray.length, filetype);
|
||||
}
|
||||
|
||||
|
||||
public class Download {
|
||||
@ -296,7 +297,7 @@ public class FileTransferHandler implements MessageHandler {
|
||||
|
||||
private FileTransferDataMessage take() {
|
||||
final int currentOffset = this.dataHolder.position();
|
||||
final byte[] chunk = new byte[Math.min(this.dataHolder.remaining(), getMaxBlockSize())];
|
||||
final byte[] chunk = new byte[Math.min(this.dataHolder.remaining(), getMaxBlockSize() - 13)]; //actual payload in FileTransferDataMessage
|
||||
this.dataHolder.get(chunk);
|
||||
setRunningCrc(ChecksumCalculator.computeCrc(getRunningCrc(), chunk, 0, chunk.length));
|
||||
return new FileTransferDataMessage(chunk, currentOffset, getRunningCrc());
|
||||
|
@ -79,6 +79,7 @@ public class FileType {
|
||||
FBT_PTD_BACKUP(128, 74),
|
||||
|
||||
// Other files
|
||||
DOWNLOAD_COURSE(255, 4),
|
||||
ERROR_SHUTDOWN_REPORTS(255, 245),
|
||||
IQ_ERROR_REPORTS(255, 244),
|
||||
ULF_LOGS(255, 247),
|
||||
|
@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminGpxRouteInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
@ -797,6 +798,14 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
final GarminGpxRouteInstallHandler garminGpxRouteInstallHandler = new GarminGpxRouteInstallHandler(uri, getContext());
|
||||
if (garminGpxRouteInstallHandler.isValid()) {
|
||||
communicator.sendMessage("upload course file", fileTransferHandler.initiateUpload(garminGpxRouteInstallHandler.getGpxRouteFileConverter().getConvertedFile().getOutgoingMessage(), FileType.FILETYPE.DOWNLOAD_COURSE).getOutgoingMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
parseAllFitFilesFromStorage();
|
||||
|
@ -10,10 +10,6 @@ public final class GarminUtils {
|
||||
// utility class
|
||||
}
|
||||
|
||||
public static double semicirclesToDegrees(final long semicircles) {
|
||||
return semicircles * (180.0D / 0x80000000L);
|
||||
}
|
||||
|
||||
public static GdiCore.CoreService.LocationData toLocationData(final Location location, final GdiCore.CoreService.DataType dataType) {
|
||||
final GdiCore.CoreService.LatLon positionForWatch = GdiCore.CoreService.LatLon.newBuilder()
|
||||
.setLat((int) ((location.getLatitude() * 2.147483648E9d) / 180.0d))
|
||||
|
@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionDayOfWeek;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource;
|
||||
@ -47,6 +48,8 @@ public class FieldDefinitionFactory {
|
||||
return new FieldDefinitionSleepStage(localNumber, size, baseType, name);
|
||||
case WEATHER_AQI:
|
||||
return new FieldDefinitionWeatherAqi(localNumber, size, baseType, name);
|
||||
case COORDINATE:
|
||||
return new FieldDefinitionCoordinate(localNumber, size, baseType, name);
|
||||
default:
|
||||
return new FieldDefinition(localNumber, size, baseType, name);
|
||||
}
|
||||
@ -66,5 +69,6 @@ public class FieldDefinitionFactory {
|
||||
LANGUAGE,
|
||||
SLEEP_STAGE,
|
||||
WEATHER_AQI,
|
||||
COORDINATE
|
||||
}
|
||||
}
|
||||
|
@ -144,7 +144,12 @@ public class FitFile {
|
||||
this.header.generateOutgoingDataPayload(writer);
|
||||
writer.writeBytes(temporary.getBytes());
|
||||
writer.writeShort(ChecksumCalculator.computeCrc(writer.getBytes(), this.header.getHeaderSize(), writer.getBytes().length - this.header.getHeaderSize()));
|
||||
}
|
||||
|
||||
public byte[] getOutgoingMessage() {
|
||||
final MessageWriter writer = new MessageWriter();
|
||||
this.generateOutgoingDataPayload(writer);
|
||||
return writer.getBytes();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -91,8 +91,8 @@ public class GlobalFITMessage {
|
||||
new FieldDefinitionPrimitive(0, BaseType.ENUM, "event"), // 9 lap
|
||||
new FieldDefinitionPrimitive(1, BaseType.ENUM, "event_type"), // 1 stop
|
||||
new FieldDefinitionPrimitive(2, BaseType.UINT32, "start_time"),
|
||||
new FieldDefinitionPrimitive(3, BaseType.SINT32, "start_latitude"),
|
||||
new FieldDefinitionPrimitive(4, BaseType.SINT32, "start_longitude"),
|
||||
new FieldDefinitionPrimitive(3, BaseType.SINT32, "start_latitude", FieldDefinitionFactory.FIELD.COORDINATE),
|
||||
new FieldDefinitionPrimitive(4, BaseType.SINT32, "start_longitude", FieldDefinitionFactory.FIELD.COORDINATE),
|
||||
new FieldDefinitionPrimitive(5, BaseType.ENUM, "sport"),
|
||||
new FieldDefinitionPrimitive(6, BaseType.ENUM, "sub_sport"),
|
||||
new FieldDefinitionPrimitive(7, BaseType.UINT32, "total_elapsed_time"), // with pauses
|
||||
@ -106,16 +106,27 @@ public class GlobalFITMessage {
|
||||
));
|
||||
|
||||
public static GlobalFITMessage LAP = new GlobalFITMessage(19, "LAP", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(3, BaseType.SINT32, "start_lat", FieldDefinitionFactory.FIELD.COORDINATE),
|
||||
new FieldDefinitionPrimitive(4, BaseType.SINT32, "start_long", FieldDefinitionFactory.FIELD.COORDINATE),
|
||||
new FieldDefinitionPrimitive(5, BaseType.SINT32, "end_lat", FieldDefinitionFactory.FIELD.COORDINATE),
|
||||
new FieldDefinitionPrimitive(6, BaseType.SINT32, "end_long", FieldDefinitionFactory.FIELD.COORDINATE),
|
||||
new FieldDefinitionPrimitive(7, BaseType.UINT32, "total_elapsed_time", 1000, 0), // s
|
||||
new FieldDefinitionPrimitive(8, BaseType.UINT32, "total_timer_time", 1000, 0), // s
|
||||
new FieldDefinitionPrimitive(9, BaseType.UINT32, "total_distance", 100, 0), // m
|
||||
new FieldDefinitionPrimitive(21, BaseType.UINT16, "total_ascent"), // m
|
||||
new FieldDefinitionPrimitive(22, BaseType.UINT16, "total_descent"), // m
|
||||
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
|
||||
));
|
||||
|
||||
public static GlobalFITMessage RECORD = new GlobalFITMessage(20, "RECORD", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.SINT32, "latitude"),
|
||||
new FieldDefinitionPrimitive(1, BaseType.SINT32, "longitude"),
|
||||
new FieldDefinitionPrimitive(0, BaseType.SINT32, "latitude", FieldDefinitionFactory.FIELD.COORDINATE),
|
||||
new FieldDefinitionPrimitive(1, BaseType.SINT32, "longitude", FieldDefinitionFactory.FIELD.COORDINATE),
|
||||
new FieldDefinitionPrimitive(2, BaseType.UINT16, "altitude", 5, 500), // m
|
||||
new FieldDefinitionPrimitive(3, BaseType.UINT8, "heart_rate"),
|
||||
new FieldDefinitionPrimitive(5, BaseType.UINT32, "distance", 100, 0), // m
|
||||
new FieldDefinitionPrimitive(6, BaseType.UINT16, "speed", 1000, 0), // m/s
|
||||
new FieldDefinitionPrimitive(73, BaseType.UINT32, "enhanced_speed"), // mm/s
|
||||
new FieldDefinitionPrimitive(78, BaseType.UINT32, "enhanced_altitude"), // dm
|
||||
new FieldDefinitionPrimitive(78, BaseType.UINT32, "enhanced_altitude", 5, 500), // m
|
||||
new FieldDefinitionPrimitive(136, BaseType.UINT8, "wrist_heart_rate"),
|
||||
new FieldDefinitionPrimitive(143, BaseType.UINT8, "body_battery"),
|
||||
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
|
||||
@ -137,6 +148,11 @@ public class GlobalFITMessage {
|
||||
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
|
||||
));
|
||||
|
||||
public static GlobalFITMessage COURSE = new GlobalFITMessage(31, "COURSE", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(4, BaseType.ENUM, "sport"),
|
||||
new FieldDefinitionPrimitive(5, BaseType.STRING, 16, "name")
|
||||
));
|
||||
|
||||
public static GlobalFITMessage FILE_CREATOR = new GlobalFITMessage(49, "FILE_CREATOR", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.UINT16, "software_version"),
|
||||
new FieldDefinitionPrimitive(1, BaseType.UINT8, "hardware_version")
|
||||
@ -282,6 +298,7 @@ public class GlobalFITMessage {
|
||||
put(20, RECORD);
|
||||
put(21, EVENT);
|
||||
put(23, DEVICE_INFO);
|
||||
put(31, COURSE);
|
||||
put(49, FILE_CREATOR);
|
||||
put(55, MONITORING);
|
||||
put(127, CONNECTIVITY);
|
||||
|
@ -0,0 +1,196 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecordDataFactory;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackSegment;
|
||||
|
||||
public class GpxRouteFileConverter {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GpxRouteFileConverter.class);
|
||||
final double speed = 1.4; // m/s // TODO: make this configurable (and activity dependent?)
|
||||
final int activity = GarminSport.RUN.getType(); //TODO: make this configurable
|
||||
private final long timestamp;
|
||||
private final GpxFile gpxFile;
|
||||
private FitFile convertedFile;
|
||||
private String name;
|
||||
|
||||
public GpxRouteFileConverter(byte[] xmlBytes) {
|
||||
this.timestamp = System.currentTimeMillis() / 1000;
|
||||
this.gpxFile = GpxParser.parseGpx(xmlBytes);
|
||||
try {
|
||||
this.convertedFile = convertGpxToRoute(gpxFile);
|
||||
} catch (Exception e) {
|
||||
LOG.error(e.getMessage());
|
||||
this.convertedFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static RecordData getFileCreatorRecordData() {
|
||||
final RecordData fileCreatorRecord = FitRecordDataFactory.create(
|
||||
new RecordDefinition(new RecordHeader((byte) 0x41), ByteOrder.BIG_ENDIAN, GlobalFITMessage.FILE_CREATOR, GlobalFITMessage.FILE_CREATOR.getFieldDefinitions(0), null),
|
||||
new RecordHeader((byte) 0x01));
|
||||
fileCreatorRecord.setFieldByName("software_version", 1);
|
||||
return fileCreatorRecord;
|
||||
}
|
||||
|
||||
public FitFile getConvertedFile() {
|
||||
return convertedFile;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return this.convertedFile != null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if (gpxFile == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!StringUtils.isNullOrEmpty(this.name))
|
||||
return this.name;
|
||||
|
||||
if (!StringUtils.isNullOrEmpty(gpxFile.getName())) {
|
||||
return gpxFile.getName();
|
||||
} else {
|
||||
return String.valueOf(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private FitFile convertGpxToRoute(GpxFile gpxFile) {
|
||||
if (gpxFile.getTracks().isEmpty()) {
|
||||
LOG.error("Gpx file contains no Tracks.");
|
||||
return null;
|
||||
}
|
||||
//GPX files may contain multiple tracks, we use only the first one
|
||||
final GpxTrack track = gpxFile.getTracks().get(0);
|
||||
|
||||
if (track.getTrackSegments().isEmpty()) {
|
||||
LOG.error("Gpx track contains no segment.");
|
||||
return null;
|
||||
}
|
||||
//GPX track may contain multiple segments, we use only the first one
|
||||
GpxTrackSegment gpxTrackSegment = track.getTrackSegments().get(0);
|
||||
|
||||
List<GpxTrackPoint> gpxTrackPointList = gpxTrackSegment.getTrackPoints();
|
||||
if (gpxTrackPointList.isEmpty()) {
|
||||
LOG.error("Gpx track segment contains no point");
|
||||
return null;
|
||||
}
|
||||
|
||||
this.name = track.getName();
|
||||
|
||||
final RecordHeader gpxDataPointRecordHeader = new RecordHeader((byte) 0x05);
|
||||
final RecordDefinition gpxDataPointRecordDefinition = new RecordDefinition(new RecordHeader((byte) 0x45), ByteOrder.BIG_ENDIAN, GlobalFITMessage.RECORD, GlobalFITMessage.RECORD.getFieldDefinitions(0, 1, 2, 5, 253), null);
|
||||
List<RecordData> gpxPointDataRecords = new ArrayList<>();
|
||||
|
||||
double totalAscent = 0;
|
||||
double totalDescent = 0;
|
||||
double totalDistance = 0;
|
||||
long runningTs = timestamp;
|
||||
|
||||
GPSCoordinate prevPoint = gpxTrackPointList.get(0);
|
||||
|
||||
for (GPSCoordinate point :
|
||||
gpxTrackPointList) {
|
||||
totalAscent += point.getAscent(prevPoint);
|
||||
totalDescent += point.getDescent(prevPoint);
|
||||
totalDistance += point.getDistance(prevPoint);
|
||||
runningTs += (long) (point.getDistance(prevPoint) / speed);
|
||||
final RecordData gpxDataPointRecord = FitRecordDataFactory.create(gpxDataPointRecordDefinition, gpxDataPointRecordHeader);
|
||||
|
||||
gpxDataPointRecord.setFieldByName("latitude", point.getLatitude());
|
||||
gpxDataPointRecord.setFieldByName("longitude", point.getLongitude());
|
||||
gpxDataPointRecord.setFieldByName("altitude", point.getAltitude());
|
||||
gpxDataPointRecord.setFieldByName("distance", totalDistance);
|
||||
gpxDataPointRecord.setFieldByName("timestamp", runningTs);
|
||||
|
||||
prevPoint = point;
|
||||
gpxPointDataRecords.add(gpxDataPointRecord);
|
||||
}
|
||||
|
||||
final RecordData lapRecord = getLapRecordData(gpxTrackPointList);
|
||||
lapRecord.setFieldByName("total_distance", totalDistance);
|
||||
lapRecord.setFieldByName("total_ascent", totalAscent);
|
||||
lapRecord.setFieldByName("total_descent", totalDescent);
|
||||
lapRecord.setFieldByName("total_elapsed_time", (runningTs - timestamp));
|
||||
lapRecord.setFieldByName("total_timer_time", (runningTs - timestamp));
|
||||
|
||||
final List<RecordData> courseFileDataRecords = new ArrayList<>();
|
||||
courseFileDataRecords.add(getFileIdRecordData());
|
||||
courseFileDataRecords.add(getFileCreatorRecordData());
|
||||
courseFileDataRecords.add(getCourseRecordData());
|
||||
courseFileDataRecords.add(lapRecord);
|
||||
|
||||
final RecordHeader eventRecordHeader = new RecordHeader((byte) 0x04);
|
||||
final RecordDefinition eventRecordDefinition = new RecordDefinition(new RecordHeader((byte) 0x44), ByteOrder.BIG_ENDIAN, GlobalFITMessage.EVENT, GlobalFITMessage.EVENT.getFieldDefinitions(0, 1, 4, 253), null);
|
||||
courseFileDataRecords.add(getEventRecordData(eventRecordDefinition, eventRecordHeader, timestamp, 0));
|
||||
courseFileDataRecords.add(getEventRecordData(eventRecordDefinition, eventRecordHeader, runningTs, 9));
|
||||
|
||||
courseFileDataRecords.addAll(gpxPointDataRecords);
|
||||
|
||||
return new FitFile(courseFileDataRecords);
|
||||
}
|
||||
|
||||
private RecordData getEventRecordData(RecordDefinition eventRecordDefinition, RecordHeader eventRecordHeader, long timestamp, int eventType) {
|
||||
final RecordData startEvent = FitRecordDataFactory.create(
|
||||
eventRecordDefinition,
|
||||
eventRecordHeader);
|
||||
|
||||
startEvent.setFieldByName("timestamp", timestamp);
|
||||
startEvent.setFieldByName("event", 0);
|
||||
startEvent.setFieldByName("event_group", 0);
|
||||
startEvent.setFieldByName("event_type", eventType);
|
||||
return startEvent;
|
||||
}
|
||||
|
||||
private RecordData getLapRecordData(List<GpxTrackPoint> gpxTrackPointList) {
|
||||
final GPSCoordinate first = gpxTrackPointList.get(0);
|
||||
final GPSCoordinate last = gpxTrackPointList.get(gpxTrackPointList.size() - 1);
|
||||
|
||||
final RecordData lapRecord = FitRecordDataFactory.create(
|
||||
new RecordDefinition(new RecordHeader((byte) 0x43), ByteOrder.BIG_ENDIAN, GlobalFITMessage.LAP, GlobalFITMessage.LAP.getFieldDefinitions(3, 4, 5, 6, 7, 8, 9, 21, 22, 253), null),
|
||||
new RecordHeader((byte) 0x03));
|
||||
lapRecord.setFieldByName("start_lat", first.getLatitude());
|
||||
lapRecord.setFieldByName("start_long", first.getLongitude());
|
||||
lapRecord.setFieldByName("end_lat", last.getLatitude());
|
||||
lapRecord.setFieldByName("end_long", last.getLongitude());
|
||||
lapRecord.setFieldByName("timestamp", timestamp);
|
||||
return lapRecord;
|
||||
}
|
||||
|
||||
private RecordData getCourseRecordData() {
|
||||
final RecordData courseRecord = FitRecordDataFactory.create(
|
||||
new RecordDefinition(new RecordHeader((byte) 0x42), ByteOrder.BIG_ENDIAN, GlobalFITMessage.COURSE, GlobalFITMessage.COURSE.getFieldDefinitions(4, 5), null),
|
||||
new RecordHeader((byte) 0x02));
|
||||
courseRecord.setFieldByName("sport", activity); //TODO use track.getType()
|
||||
courseRecord.setFieldByName("name", this.getName());
|
||||
return courseRecord;
|
||||
}
|
||||
|
||||
private RecordData getFileIdRecordData() {
|
||||
final RecordData fileIdRecord = FitRecordDataFactory.create(
|
||||
new RecordDefinition(new RecordHeader((byte) 0x40), ByteOrder.BIG_ENDIAN, GlobalFITMessage.FILE_ID, GlobalFITMessage.FILE_ID.getFieldDefinitions(0, 1, 2, 3, 4, 5), null),
|
||||
new RecordHeader((byte) 0x00));
|
||||
fileIdRecord.setFieldByName("type", FileType.FILETYPE.COURSES.getSubType());
|
||||
fileIdRecord.setFieldByName("manufacturer", 1);
|
||||
fileIdRecord.setFieldByName("product", 65534);
|
||||
fileIdRecord.setFieldByName("time_created", timestamp);
|
||||
fileIdRecord.setFieldByName("serial_number", 1);
|
||||
fileIdRecord.setFieldByName("number", 1);
|
||||
return fileIdRecord;
|
||||
}
|
||||
|
||||
}
|
@ -69,7 +69,7 @@ public class RecordHeader {
|
||||
|
||||
public byte generateOutgoingDataPayload() { //TODO: unclear if correct
|
||||
if (!definition && !developerData) {
|
||||
assert timeOffset != null;
|
||||
if (timeOffset != null)
|
||||
return (byte) (timeOffset | (((byte) localMessageType) << 5));
|
||||
}
|
||||
byte base = (byte) localMessageType;
|
||||
|
@ -252,6 +252,8 @@ public class FitCodeGen {
|
||||
return FieldDefinitionSleepStage.SleepStage.class;
|
||||
case WEATHER_AQI:
|
||||
return FieldDefinitionWeatherAqi.AQI_LEVELS.class;
|
||||
case COORDINATE:
|
||||
return Double.class;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unknown field type " + primitive.getType());
|
||||
|
@ -0,0 +1,27 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionCoordinate extends FieldDefinition {
|
||||
|
||||
final double conversionFactor = (180.0D / 0x80000000L);
|
||||
|
||||
public FieldDefinitionCoordinate(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
return ((long) baseType.decode(byteBuffer, 1, 0)) * conversionFactor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
baseType.encode(byteBuffer, (int) Math.round((double) o / conversionFactor), 1, 0);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
||||
|
||||
//
|
||||
// WARNING: This class was auto-generated, please avoid modifying it directly.
|
||||
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
|
||||
//
|
||||
public class FitCourse extends RecordData {
|
||||
public FitCourse(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
|
||||
super(recordDefinition, recordHeader);
|
||||
|
||||
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
|
||||
if (globalNumber != 31) {
|
||||
throw new IllegalArgumentException("FitCourse expects global messages of " + 31 + ", got " + globalNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getSport() {
|
||||
return (Integer) getFieldByNumber(4);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getName() {
|
||||
return (String) getFieldByNumber(5);
|
||||
}
|
||||
}
|
@ -20,6 +20,51 @@ public class FitLap extends RecordData {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Double getStartLat() {
|
||||
return (Double) getFieldByNumber(3);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Double getStartLong() {
|
||||
return (Double) getFieldByNumber(4);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Double getEndLat() {
|
||||
return (Double) getFieldByNumber(5);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Double getEndLong() {
|
||||
return (Double) getFieldByNumber(6);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getTotalElapsedTime() {
|
||||
return (Long) getFieldByNumber(7);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getTotalTimerTime() {
|
||||
return (Long) getFieldByNumber(8);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getTotalDistance() {
|
||||
return (Long) getFieldByNumber(9);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getTotalAscent() {
|
||||
return (Integer) getFieldByNumber(21);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getTotalDescent() {
|
||||
return (Integer) getFieldByNumber(22);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getTimestamp() {
|
||||
return (Long) getFieldByNumber(253);
|
||||
|
@ -26,13 +26,18 @@ public class FitRecord extends RecordData {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getLatitude() {
|
||||
return (Long) getFieldByNumber(0);
|
||||
public Double getLatitude() {
|
||||
return (Double) getFieldByNumber(0);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getLongitude() {
|
||||
return (Long) getFieldByNumber(1);
|
||||
public Double getLongitude() {
|
||||
return (Double) getFieldByNumber(1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getAltitude() {
|
||||
return (Integer) getFieldByNumber(2);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@ -45,6 +50,11 @@ public class FitRecord extends RecordData {
|
||||
return (Long) getFieldByNumber(5);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getSpeed() {
|
||||
return (Integer) getFieldByNumber(6);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getEnhancedSpeed() {
|
||||
return (Long) getFieldByNumber(73);
|
||||
@ -77,9 +87,9 @@ public class FitRecord extends RecordData {
|
||||
activityPoint.setTime(new Date(getComputedTimestamp()));
|
||||
if (getLatitude() != null && getLongitude() != null) {
|
||||
activityPoint.setLocation(new GPSCoordinate(
|
||||
GarminUtils.semicirclesToDegrees(getLongitude().longValue()),
|
||||
GarminUtils.semicirclesToDegrees(getLatitude().longValue()),
|
||||
getEnhancedAltitude() != null ? getEnhancedAltitude() / 10d : GPSCoordinate.UNKNOWN_ALTITUDE
|
||||
getLongitude(),
|
||||
getLatitude(),
|
||||
getEnhancedAltitude() != null ? getEnhancedAltitude() : GPSCoordinate.UNKNOWN_ALTITUDE
|
||||
));
|
||||
}
|
||||
if (getHeartRate() != null) {
|
||||
|
@ -37,6 +37,8 @@ public class FitRecordDataFactory {
|
||||
return new FitEvent(recordDefinition, recordHeader);
|
||||
case 23:
|
||||
return new FitDeviceInfo(recordDefinition, recordHeader);
|
||||
case 31:
|
||||
return new FitCourse(recordDefinition, recordHeader);
|
||||
case 49:
|
||||
return new FitFileCreator(recordDefinition, recordHeader);
|
||||
case 55:
|
||||
|
@ -18,12 +18,9 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operat
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -31,9 +28,7 @@ import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackPoint;
|
||||
@ -44,15 +39,6 @@ public class ZeppOsGpxRouteFile {
|
||||
|
||||
private static final double COORD_MULTIPLIER = 3000000.0;
|
||||
|
||||
public static final byte[] XML_HEADER = new byte[]{
|
||||
'<', '?', 'x', 'm', 'l'
|
||||
};
|
||||
|
||||
// Some gpx files start with "<gpx" directly.. this needs to be improved
|
||||
public static final byte[] GPX_START = new byte[]{
|
||||
'<', 'g', 'p', 'x'
|
||||
};
|
||||
|
||||
private final byte[] xmlBytes;
|
||||
private final long timestamp;
|
||||
private final GpxFile gpxFile;
|
||||
@ -60,36 +46,13 @@ public class ZeppOsGpxRouteFile {
|
||||
public ZeppOsGpxRouteFile(final byte[] xmlBytes) {
|
||||
this.xmlBytes = xmlBytes;
|
||||
this.timestamp = System.currentTimeMillis() / 1000;
|
||||
this.gpxFile = parseGpx(xmlBytes);
|
||||
this.gpxFile = GpxParser.parseGpx(xmlBytes);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return this.gpxFile != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static GpxFile parseGpx(final byte[] xmlBytes) {
|
||||
if (!isGpxFile(xmlBytes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(xmlBytes)) {
|
||||
final GpxParser gpxParser = new GpxParser(bais);
|
||||
return gpxParser.getGpxFile();
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to read xml", e);
|
||||
} catch (final GpxParseException e) {
|
||||
LOG.error("Failed to parse gpx", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isGpxFile(final byte[] data) {
|
||||
// TODO improve this
|
||||
return ArrayUtils.equals(data, XML_HEADER, 0) || ArrayUtils.equals(data, GPX_START, 0);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
@ -108,7 +71,6 @@ public class ZeppOsGpxRouteFile {
|
||||
|
||||
public byte[] getEncodedBytes() {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
final GpxFile gpxFile = parseGpx(xmlBytes);
|
||||
if (gpxFile == null) {
|
||||
LOG.error("Failed to read gpx file - this should never happen");
|
||||
return null;
|
||||
|
@ -16,6 +16,8 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.util.gpx;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.internal.bind.util.ISO8601Utils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -24,12 +26,14 @@ import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParsePosition;
|
||||
import java.util.Date;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackPoint;
|
||||
@ -39,11 +43,44 @@ import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxWaypoint;
|
||||
public class GpxParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GpxParser.class);
|
||||
|
||||
public static final byte[] XML_HEADER = new byte[]{
|
||||
'<', '?', 'x', 'm', 'l'
|
||||
};
|
||||
|
||||
// Some gpx files start with "<gpx" directly.. this needs to be improved
|
||||
public static final byte[] GPX_START = new byte[]{
|
||||
'<', 'g', 'p', 'x'
|
||||
};
|
||||
|
||||
private final XmlPullParser parser;
|
||||
private int eventType;
|
||||
|
||||
private final GpxFile.Builder fileBuilder;
|
||||
|
||||
|
||||
@Nullable
|
||||
public static GpxFile parseGpx(final byte[] xmlBytes) {
|
||||
if (!isGpxFile(xmlBytes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(xmlBytes)) {
|
||||
final GpxParser gpxParser = new GpxParser(bais);
|
||||
return gpxParser.getGpxFile();
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to read xml", e);
|
||||
} catch (final GpxParseException e) {
|
||||
LOG.error("Failed to parse gpx", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isGpxFile(final byte[] data) {
|
||||
// TODO improve this
|
||||
return ArrayUtils.equals(data, XML_HEADER, 0) || ArrayUtils.equals(data, GPX_START, 0);
|
||||
}
|
||||
|
||||
public GpxParser(final InputStream stream) throws GpxParseException {
|
||||
this.fileBuilder = new GpxFile.Builder();
|
||||
|
||||
@ -98,6 +135,12 @@ public class GpxParser {
|
||||
while (eventType != XmlPullParser.END_TAG || !parser.getName().equals("trk")) {
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
switch (parser.getName()) {
|
||||
case "name":
|
||||
trackBuilder.withName(parseStringContent("name"));
|
||||
continue;
|
||||
case "type":
|
||||
trackBuilder.withType(parseStringContent("type"));
|
||||
continue;
|
||||
case "trkseg":
|
||||
final GpxTrackSegment segment = parseTrackSegment();
|
||||
if (!segment.getTrackPoints().isEmpty()) {
|
||||
|
@ -20,12 +20,25 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GpxTrack {
|
||||
private final String name;
|
||||
|
||||
private final String type;
|
||||
private final List<GpxTrackSegment> trackSegments;
|
||||
|
||||
public GpxTrack(final List<GpxTrackSegment> trackSegments) {
|
||||
public GpxTrack(String name, String type, final List<GpxTrackSegment> trackSegments) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.trackSegments = trackSegments;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public List<GpxTrackSegment> getTrackSegments() {
|
||||
return trackSegments;
|
||||
}
|
||||
@ -41,15 +54,27 @@ public class GpxTrack {
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String name;
|
||||
private String type;
|
||||
private final List<GpxTrackSegment> trackSegments = new ArrayList<>();
|
||||
|
||||
public Builder withName(final String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withType(final String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withTrackSegment(final GpxTrackSegment trackSegment) {
|
||||
trackSegments.add(trackSegment);
|
||||
return this;
|
||||
}
|
||||
|
||||
public GpxTrack build() {
|
||||
return new GpxTrack(trackSegments);
|
||||
return new GpxTrack(name, type, trackSegments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -364,9 +364,9 @@ public class GarminSupportTest extends TestBase {
|
||||
"FitFileId{manufacturer=15, type=ACTIVITY, product=9001, serial_number=1701}, " +
|
||||
"FitDeveloperData{application_id=[1,1,2,3,5,8,13,21,34,55,89,144,233,121,98,219], developer_data_index=0}, " +
|
||||
"FitFieldDescription{developer_data_index=0, field_definition_number=0, fit_base_type_id=1, field_name=doughnuts_earned, units=doughnuts}, " +
|
||||
"FitRecord{heart_rate=140, unknown_4(UINT8/1)=88, distance=510, unknown_6(UINT16/2)=47488, doughnuts_earned=1}, " +
|
||||
"FitRecord{heart_rate=143, unknown_4(UINT8/1)=90, distance=2080, unknown_6(UINT16/2)=36416, doughnuts_earned=2}, " +
|
||||
"FitRecord{heart_rate=144, unknown_4(UINT8/1)=92, distance=3710, unknown_6(UINT16/2)=35344, doughnuts_earned=3}" +
|
||||
"FitRecord{heart_rate=140, unknown_4(UINT8/1)=88, distance=510, speed=47, doughnuts_earned=1}, " +
|
||||
"FitRecord{heart_rate=143, unknown_4(UINT8/1)=90, distance=2080, speed=36, doughnuts_earned=2}, " +
|
||||
"FitRecord{heart_rate=144, unknown_4(UINT8/1)=92, distance=3710, speed=35, doughnuts_earned=3}" +
|
||||
"]";
|
||||
|
||||
FitFile fitFile = FitFile.parseIncoming(fileContents);
|
||||
|
Loading…
Reference in New Issue
Block a user