Garmin: Fix MLR retransmission request numbers

This commit is contained in:
José Rebelo
2026-01-16 18:24:19 +00:00
parent 6aeb50d026
commit f4556e81cc
5 changed files with 66 additions and 9 deletions
@@ -821,7 +821,8 @@ public interface DeviceCoordinator {
int[] getSupportedDeviceSpecificAuthenticationSettings();
/**
* Returns device specific debug settings. This section is only shown in debug builds.
* Returns device specific debug settings. This section is only shown in debug builds, and all behavior-altering
* preferences should be gate-kept by the BuildConfig.DEBUG flag.
*/
int[] getSupportedDebugSettings(final GBDevice device);
@@ -8,6 +8,8 @@ import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -228,6 +230,14 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return new int[]{R.xml.devicesettings_garmin_experimental};
}
@Override
public int[] getSupportedDebugSettings(final GBDevice device) {
return ArrayUtils.add(
super.getSupportedDebugSettings(device),
R.xml.devicesettings_debug_drop_packets
);
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
@@ -21,7 +21,9 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
@@ -161,6 +163,18 @@ public class CommunicatorV2 implements ICommunicator {
final int handle = ((value[0] & MlrCommunicator.HANDLE_MASK) >> 4) | MlrCommunicator.MLR_FLAG_MASK;
final MlrCommunicator mlrComm = mlrCommunicators.get(handle);
if (mlrComm != null) {
if (BuildConfig.DEBUG) {
final int packetPercentageIn = mSupport.getDevicePrefs().getInt("pref_debug_drop_packet_percentage_in", 0);
if (packetPercentageIn > 0 && ThreadLocalRandom.current().nextInt(100) < packetPercentageIn) {
LOG.warn(
"Simulating dropped inbound packet handle={}, dataLen={}",
handle,
value.length
);
return true;
}
}
mlrComm.onPacketReceived(value);
return true;
}
@@ -639,9 +653,18 @@ public class CommunicatorV2 implements ICommunicator {
}
private MlrCommunicator createMlrCommunicator(final int handle, final ServiceCallback callback) {
final MlrCommunicator.MessageSender messageSender = (taskName, packet) -> mSupport.createTransactionBuilder(taskName)
.write(characteristicSend, packet)
.queue();
final MlrCommunicator.MessageSender messageSender = (taskName, packet) -> {
if (BuildConfig.DEBUG) {
final int packetPercentageOut = mSupport.getDevicePrefs().getInt("pref_debug_drop_packet_percentage_out", 0);
if (packetPercentageOut > 0 && ThreadLocalRandom.current().nextInt(100) < packetPercentageOut) {
LOG.warn("Simulating dropped outbound packet for {} ({})", taskName, GB.hexdump(packet));
return;
}
}
mSupport.createTransactionBuilder(taskName)
.write(characteristicSend, packet)
.queue();
};
return new MlrCommunicator(handle, maxWriteSize, messageSender, callback::onMessage);
}
@@ -81,12 +81,12 @@ public class MlrCommunicator {
int position = 0;
while (remainingBytes > 0) {
final byte[] fragment = Arrays.copyOfRange(message, position, position + Math.min(remainingBytes, maxPacketSize - 2));
fragmentQueue.add(new Fragment(taskName, i++, fragment));
fragmentQueue.add(new Fragment(taskName, i++, fragment, 0));
position += fragment.length;
remainingBytes -= fragment.length;
}
} else {
fragmentQueue.add(new Fragment(taskName, 0, message));
fragmentQueue.add(new Fragment(taskName, 0, message, 0));
}
runProtocol();
@@ -219,9 +219,11 @@ public class MlrCommunicator {
// Send next fragment if available
final Fragment fragment = fragmentQueue.poll();
if (fragment != null) {
// Update the fragment with the current reqNum before storing it
final Fragment fragmentWithReqNum = new Fragment(fragment.taskName, fragment.num, fragment.data, nextRcvSeq);
final byte[] packet = createPacket(nextRcvSeq, nextSendSeq, fragment.data);
messageSender.sendPacket(fragment.taskName + " (" + fragment.num + ")", packet);
sentFragments[nextSendSeq] = fragment;
sentFragments[nextSendSeq] = fragmentWithReqNum;
nextSendSeq = (nextSendSeq + 1) % (MAX_SEQ_NUM + 1);
@@ -278,7 +280,8 @@ public class MlrCommunicator {
LOG.error("Attempting to re-send null fragment at index {}", i);
continue;
}
final byte[] packet = createPacket(nextRcvSeq, i, fragment.data);
// Use the original reqNum that was stored when the fragment was first sent
final byte[] packet = createPacket(fragment.reqNum, i, fragment.data);
messageSender.sendPacket("retransmission " + fragment.taskName + " (" + fragment.num + ")", packet);
}
@@ -307,6 +310,6 @@ public class MlrCommunicator {
void onDataReceived(final byte[] data);
}
private record Fragment(String taskName, int num, byte[] data) {
private record Fragment(String taskName, int num, byte[] data, int reqNum) {
}
}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_file_upload"
android:key="pref_debug_drop_packet_percentage_out"
android:max="100"
android:summary="Probability of failing to send a packet"
android:title="Simulate packet loss - outbound"
app:showSeekBarValue="true" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_download"
android:key="pref_debug_drop_packet_percentage_in"
android:max="100"
android:summary="Probability of failing to receive a packet"
android:title="Simulate packet loss - inbound"
app:showSeekBarValue="true" />
</androidx.preference.PreferenceScreen>