igpsport: download workouts

* now able to save all fit files from computed to phone
/sdcard/Android//data/nodomain.freeyourgadget.gadgetbridge/files/
* TODO parse fit files to workouts
This commit is contained in:
Vitaliy Tomin
2026-05-13 23:33:24 +02:00
committed by José Rebelo
parent f6aa398316
commit b193da7349
2 changed files with 230 additions and 30 deletions
@@ -1,6 +1,8 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.igpsport;
import static nodomain.freeyourgadget.gadgetbridge.devices.igpsport.IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_THIRD_RX;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
@@ -13,12 +15,14 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.UUID;
@@ -51,7 +55,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
@@ -73,6 +79,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
public final DeviceInfoProfile<IGPSportDeviceSupport> deviceInfoProfile;
public final BatteryInfoProfile<IGPSportDeviceSupport> batteryInfoProfile;
private IGPSportRoutesManager routeManager;
private IGPSportDownloadManager downloadManager;
private int mtuSize=247; //FIXME use actual device mtu
@@ -80,6 +87,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
super(LOG);
routeManager = new IGPSportRoutesManager(this);
downloadManager = new IGPSportDownloadManager(this);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE);
@@ -90,9 +98,9 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
public void notify(Intent intent) {
String action = intent.getAction();
if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) {
handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
handleDeviceInfo((DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
} else if (BatteryInfoProfile.ACTION_BATTERY_INFO.equals(action)) {
handleBatteryInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO));
handleBatteryInfo((BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO));
}
}
};
@@ -109,7 +117,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
addSupportedService(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_FIRST_SERVICE);
addSupportedService(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_SECOND_RX);
addSupportedService(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_SECOND_SERVICE);
addSupportedService(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_THIRD_RX);
addSupportedService(UUID_IGPSPORT_CHARACTERISTIC_THIRD_RX);
addSupportedService(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_THIRD_SERVICE);
addSupportedService(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_FOURTH_RX);
addSupportedService(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_FORTH_SERVICE);
@@ -130,7 +138,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
}
public void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) {
public void handleBatteryInfo(BatteryInfo info) {
LOG.debug("iGPSport battery info: " + info);
batteryCmd.level = (short) info.getPercentCharged();
handleGBDeviceEvent(batteryCmd);
@@ -147,7 +155,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
builder.notify(getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_FIRST_RX), true);
builder.notify(getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_SECOND_RX), true);
builder.notify(getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_THIRD_RX), true);
builder.notify(getCharacteristic(UUID_IGPSPORT_CHARACTERISTIC_THIRD_RX), true);
builder.notify(getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_FOURTH_RX), true);
builder.setCallback(this);
@@ -211,40 +219,70 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.info("Characteristic changed value: " + GB.hexdump(characteristic.getValue()));
byte[] data = characteristic.getValue();
if (downloadManager.needMoreData()) {
if (characteristicUUID.compareTo(UUID_IGPSPORT_CHARACTERISTIC_THIRD_RX) == 0 ) {
downloadManager.addData(data);
}
}
if (data[0] == IGPSportConstants.DATA_HEADER) {
if (data != null && data.length > 20) {
byte mainService = data[1];
byte mainOperation = data[4];
int dataSize = ByteBuffer.wrap(data, 7, 2).getShort();
byte[] pbData = new byte[dataSize];
System.arraycopy(data, 20, pbData, 0, dataSize);
if (data[3] == (byte)0xff) {
int dataSize = ByteBuffer.wrap(data, 7, 2).getShort();
try {
byte[] pbData = new byte[dataSize];
System.arraycopy(data, 20, pbData, 0, dataSize);
try {
switch (mainService) {
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_FACTORY_VALUE:
handleFactoryData(pbData);
break;
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_FIRMWARE_VALUE:
handleFirmwareData(pbData);
break;
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_CYCLING_DATA_VALUE:
if (mainOperation == CyclingData.CYCLING_DATA_OPERATE_TYPE.enum_CYCLING_DATA_OPERATE_TYPE_LIST_SEND_VALUE) {
downloadManager.setFilesAvaliable(pbData);
}
break;
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_ROUTE_PLAN_VALUE:
if (mainOperation == RoutePlan.ROUTE_PLAN_OPERATE_TYPE.enum_ROUTE_PLAN_OPERATE_TYPE_LIST_NUM_GET_VALUE) {
routeManager.handleRouteNumber(pbData);
} else if (mainOperation == RoutePlan.ROUTE_PLAN_OPERATE_TYPE.enum_ROUTE_PLAN_OPERATE_TYPE_LIST_SEND_VALUE) {
routeManager.handleRouteList(pbData);
}
break;
default:
LOG.error("Unknown general operation received");
}
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
} else if (data[3] == (byte)0x55) {
switch (mainService) {
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_FACTORY_VALUE:
handleFactoryData(pbData);
break;
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_FIRMWARE_VALUE:
handleFirmwareData(pbData);
break;
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_ROUTE_PLAN_VALUE:
if(mainOperation == RoutePlan.ROUTE_PLAN_OPERATE_TYPE.enum_ROUTE_PLAN_OPERATE_TYPE_LIST_NUM_GET_VALUE) {
routeManager.handleRouteNumber(pbData);
} else if (mainOperation == RoutePlan.ROUTE_PLAN_OPERATE_TYPE.enum_ROUTE_PLAN_OPERATE_TYPE_LIST_SEND_VALUE) {
routeManager.handleRouteList(pbData);
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_CYCLING_DATA_VALUE:
if (mainOperation == CyclingData.CYCLING_DATA_OPERATE_TYPE.enum_CYCLING_DATA_OPERATE_TYPE_FILE_SEND_VALUE) {
downloadManager.startDownload();
downloadManager.addData(data);
}
break;
default:
LOG.error("Unknown file operation received");
}
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
}
}
if (data[0] == 0x02) {
//0215FFFF03FFFF00FFFFFFFFFFFFFFFFFFFFFF55
byte mainService = data[1];
byte mainOperation = data[4];
@@ -256,7 +294,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
gbDevice.sendDeviceUpdateIntent(getContext());
TransactionBuilder builder = new TransactionBuilder("Route upload finished");
if (result == 0) {
builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction(
builder.add(new SetProgressAction(
"Route upload completed",
false,
100,
@@ -264,7 +302,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
));
handleGBDeviceEvent(new GBDeviceEventDisplayMessage("Route upload completed", Toast.LENGTH_LONG, GB.INFO));
} else {
builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction(
builder.add(new SetProgressAction(
"Route upload failed",
false,
0,
@@ -275,6 +313,12 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
builder.queue(getQueue());
}
break;
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_CYCLING_DATA_VALUE:
if(mainOperation == CyclingData.CYCLING_DATA_OPERATE_TYPE.enum_CYCLING_DATA_OPERATE_TYPE_FILE_SEND_VALUE) {
LOG.info("Got reply from file service"); // Do we have to do something here?
}
break;
}
}
@@ -380,7 +424,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
insDataMsgBuilder.setName(notificationSpec.title);
if (notificationSpec.body != null)
insDataMsgBuilder.setContent(notificationSpec.body);
String timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date());
String timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date());
insDataMsgBuilder.setTime(timeStamp);
insMsgBuilder.setInsDataMsg(insDataMsgBuilder);
byte[] callData = craftData(insMsgBuilder.getServiceType().getNumber(), insMsgBuilder.getInsServiceType().getNumber(), insMsgBuilder.getInsOperateType().getNumber(), insMsgBuilder.build().toByteArray());
@@ -408,7 +452,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
Ins.ins_data_message.Builder insDataMsgBuilder = Ins.ins_data_message.newBuilder();
insDataMsgBuilder.setTelNum(ByteString.copyFromUtf8(callSpec.number));
insDataMsgBuilder.setName(callSpec.name);
String timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date());
String timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date());
insDataMsgBuilder.setTime(timeStamp);
insMsgBuilder.setInsDataMsg(insDataMsgBuilder);
byte[] callData = craftData(insMsgBuilder.getServiceType().getNumber(), insMsgBuilder.getInsServiceType().getNumber(), insMsgBuilder.getInsOperateType().getNumber(), insMsgBuilder.build().toByteArray());
@@ -421,11 +465,10 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
TransactionBuilder builder = this.createTransactionBuilder("onfetchfitness");
CyclingData.cycling_data_msg.Builder cycleDataMsg = CyclingData.cycling_data_msg.newBuilder();
cycleDataMsg.setServiceType(Common.service_type_index.enum_SERVICE_TYPE_INDEX_CYCLING_DATA);
// cycleDataMsg.setCyclingDataOperateType(CyclingData.CYCLING_DATA_OPERATE_TYPE.enum_CYCLING_DATA_OPERATE_TYPE_LIST_GET);
cycleDataMsg.setCyclingDataOperateType(CyclingData.CYCLING_DATA_OPERATE_TYPE.enum_CYCLING_DATA_OPERATE_TYPE_LIST_GET);
// cycleDataMsg.setListMsg(Common.file_list_get_message.newBuilder().setFileIndexStart(0).setFileIndexEnd(11) );
cycleDataMsg.setCyclingDataOperateType(CyclingData.CYCLING_DATA_OPERATE_TYPE.enum_CYCLING_DATA_OPERATE_TYPE_FILE_GET);
cycleDataMsg.addCyclingDataFileFlagMsg( CyclingData.cycling_data_file_flag_message.newBuilder().setTimestamp(1092299406) ); //FIXME: hardcoded value from commented request above
// cycleDataMsg.setCyclingDataOperateType(CyclingData.CYCLING_DATA_OPERATE_TYPE.enum_CYCLING_DATA_OPERATE_TYPE_FILE_GET);
// cycleDataMsg.addCyclingDataFileFlagMsg( CyclingData.cycling_data_file_flag_message.newBuilder().setTimestamp(1092299406) ); //FIXME: hardcoded value from commented request above
byte[] cycleDataMsgBytes = craftData(cycleDataMsg.getServiceType().getNumber(), 0xff, cycleDataMsg.getCyclingDataOperateType().getNumber(), cycleDataMsg.build().toByteArray(), true);
@@ -565,4 +608,8 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
}
public File getWritableExportDirectory() throws IOException {
return getDevice().getDeviceCoordinator().getWritableExportDirectory(getDevice());
}
}
@@ -0,0 +1,153 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.igpsport;
import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.proto.igpsport.Common;
import nodomain.freeyourgadget.gadgetbridge.proto.igpsport.CyclingData;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class IGPSportDownloadManager {
Logger LOG = LoggerFactory.getLogger(nodomain.freeyourgadget.gadgetbridge.service.devices.igpsport.IGPSportDownloadManager.class);
private IGPSportDeviceSupport support = null;
private List<FileInfo> avaliableActivityFiles = new ArrayList<>();
private ByteArrayOutputStream recievingDataBuffer;
private FileInfo downloadingFile;
private Boolean downloadInProgress = false;
public IGPSportDownloadManager(IGPSportDeviceSupport support) {
this.support = support;
recievingDataBuffer = new ByteArrayOutputStream();
}
public void setFilesAvaliable(byte[] pbData) throws InvalidProtocolBufferException {
List<CyclingData.cycling_data_file_flag_message> message = CyclingData.cycling_data_msg.parseFrom(pbData).getCyclingDataFileFlagMsgList();
for (final CyclingData.cycling_data_file_flag_message fileMsg : message) {
avaliableActivityFiles.add(new FileInfo(fileMsg));
}
LOG.info("Found " + message.size() + " files");
syncNextFile();
}
public void syncNextFile() {
if (avaliableActivityFiles.isEmpty()) {
LOG.info("No files to sync");
return;
} else {
LOG.info(avaliableActivityFiles.size() + " files to sync");
}
downloadingFile = avaliableActivityFiles.remove(0);
//if (downloadingFile.getTimeStamp() == 1092299406) { // FIXME replace with newer that las sync. hardcoded for debug
TransactionBuilder builder = support.createTransactionBuilder("ongettrainingfile");
CyclingData.cycling_data_msg.Builder cycleDataMsg = CyclingData.cycling_data_msg.newBuilder();
cycleDataMsg.setServiceType(Common.service_type_index.enum_SERVICE_TYPE_INDEX_CYCLING_DATA);
cycleDataMsg.setCyclingDataOperateType(CyclingData.CYCLING_DATA_OPERATE_TYPE.enum_CYCLING_DATA_OPERATE_TYPE_FILE_GET);
cycleDataMsg.addCyclingDataFileFlagMsg( CyclingData.cycling_data_file_flag_message.newBuilder().setTimestamp(downloadingFile.getTimeStamp()) );
byte[] cycleDataMsgBytes = IGPSportDeviceSupport.craftData(cycleDataMsg.getServiceType().getNumber(), 0xff, cycleDataMsg.getCyclingDataOperateType().getNumber(), cycleDataMsg.build().toByteArray(), true);
builder.write(support.writeCharacteristicThird, cycleDataMsgBytes);
builder.queue(support.getQueue());
// } else {
// syncNextFile();
// }
}
public void startDownload() {
recievingDataBuffer.reset();
downloadInProgress = true;
}
public void addData(byte[] data) {
try {
recievingDataBuffer.write(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
LOG.debug("current data stored size: " + recievingDataBuffer.size() + " need " + downloadingFile.getFileSize());
if (recievingDataBuffer.size() > downloadingFile.getFileSize()) {
//LOG.info(GB.hexdump(recievingDataBuffer.toByteArray()));
downloadInProgress = false;
File dir;
File outputFile;
try {
dir = support.getWritableExportDirectory();
outputFile = new File(dir, downloadingFile.getFileName());
FileUtils.copyStreamToFile(new ByteArrayInputStream(recievingDataBuffer.toByteArray(), 28, recievingDataBuffer.size()-28), outputFile);
outputFile.setLastModified(downloadingFile.getTimeStamp()* 1000L);
} catch (IOException e) {
throw new RuntimeException(e);
}
syncNextFile();
}
}
public Boolean needMoreData() {
if (!downloadInProgress)
return false;
if (recievingDataBuffer.size() < downloadingFile.getFileSize()) {
return true;
} else {
return false;
}
}
public static class FileInfo {
private int timestamp = 0;
private int file_size = 0;
private String user_id = "";
private String device_id = "";
public FileInfo(CyclingData.cycling_data_file_flag_message message) {
timestamp = message.getTimestamp();
file_size = message.getFileSize();
user_id = message.getUserId();
device_id = message.getDeviceId();
}
public int getTimeStamp() {
return timestamp;
}
public String getFileName() {
long timestampMillis = timestamp * 1000L;
Instant instant = Instant.ofEpochMilli(timestampMillis);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.of("UTC"));
return zonedDateTime.format(formatter)+".fit";
}
public int getFileSize() {
return file_size;
}
}
}