fix(withings,scanwatch): Syncing shows connected in the app and becomes stale

This commit is contained in:
d3vv3
2026-04-06 15:57:20 +02:00
parent 4ea41f5866
commit dc541a680d
3 changed files with 91 additions and 29 deletions
@@ -310,7 +310,9 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
try {
getDevice().setBusyTask(R.string.busy_task_syncing, getContext());
getDevice().sendDeviceUpdateIntent(getContext());
syncInProgress = true;
logger.info("Starting Withings sync, trigger={}, shouldSync={}", triggerSource, shoudSync());
if (withingsEcgHandler == null) {
withingsEcgHandler = createEcgHandler();
}
@@ -388,6 +390,7 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
conversationQueue.clear();
} finally {
// This must be done in all cases or the watch won't respond anymore!
logger.debug("Queueing SYNC_OK terminator for trigger={}", triggerSource);
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SYNC_OK), new SyncFinishedHandler(this));
}
conversationQueue.send();
@@ -455,6 +458,17 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
}
}
@Override
public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothGatt.STATE_CONNECTED) {
clearStaleSyncState("connect");
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
clearStaleSyncState("disconnect");
}
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
notificationProvider.notifyClient(notificationSpec);
@@ -670,7 +684,28 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
logger.debug("Finished initialization.");
}
private void clearStaleSyncState(final String reason) {
if (withingsEcgHandler != null) {
withingsEcgHandler.reset();
} else {
GB.updateTransferNotification(null, "", false, 100, getContext());
}
if (syncInProgress) {
logger.info("Clearing stale Withings sync state on {}", reason);
syncInProgress = false;
conversationQueue.clear();
}
final String syncingTask = getContext().getString(R.string.busy_task_syncing);
if (syncingTask.equals(getDevice().getBusyTask())) {
getDevice().unsetBusyTask();
getDevice().sendDeviceUpdateIntent(getContext());
}
}
public void finishSync() {
logger.info("Finishing Withings sync, busyTask={}, syncInProgress={}", getDevice().getBusyTask(), syncInProgress);
syncInProgress = false;
if (withingsEcgHandler != null) {
withingsEcgHandler.onSyncFinished();
@@ -16,11 +16,15 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
public class SyncFinishedHandler extends AbstractResponseHandler {
private static final Logger logger = LoggerFactory.getLogger(SyncFinishedHandler.class);
public SyncFinishedHandler(WithingsBaseDeviceSupport support) {
super(support);
@@ -30,6 +34,7 @@ public class SyncFinishedHandler extends AbstractResponseHandler {
@Override
public void handleResponse(Message response) {
logger.info("SyncFinishedHandler received response type={} (0x{})", response.getType(), Integer.toHexString(response.getType() & 0xffff));
if (response.getType() == WithingsMessageType.SYNC_OK) {
support.finishSync();
}
@@ -106,6 +106,7 @@ public class WithingsEcgHandler implements ResponseHandler {
discoveredEcgCount = 0;
fetchedEcgCount = 0;
deletedEcgCount = 0;
logger.info("Starting Withings ECG discovery");
updateEcgProgress("Scanning for ECG records", 0, true);
queueDiscoveryProbe(true);
}
@@ -198,6 +199,8 @@ public class WithingsEcgHandler implements ResponseHandler {
// Keep the discovery conversation active until its trailing MEASURE_STOP / TRANSFER_COMPLETE.
// Otherwise that trailing completion marker can get mis-bound to the next queued 0x0147
// request, causing the actual ECG waveform stream to be routed into a stored-measure handler.
logger.debug("Queueing Withings ECG discovery probe, initial={}, discoveryPagesSeen={}, waveformFetchQueued={}",
initial, discoveryPagesSeen, waveformFetchQueued);
final WithingsMessage message = new WithingsMessage(WithingsMessageType.MEASURE_START, ExpectedResponse.EOT);
message.addDataStructure(new MeasureCategory(MeasureCategory.ECG));
message.addDataStructure(new MeasureLiveAppStatus(initial ? 1 : 0));
@@ -207,6 +210,7 @@ public class WithingsEcgHandler implements ResponseHandler {
private void handleDiscoveryResponse(final Message response) {
final List<WithingsStructure> structures = response.getDataStructures();
if (structures == null || structures.isEmpty()) {
logger.warn("Withings ECG discovery response type={} had no structures", response.getType());
return;
}
@@ -226,6 +230,9 @@ public class WithingsEcgHandler implements ResponseHandler {
}
}
logger.debug("Withings ECG discovery response type={} hasEot={} hasEcgMarkers={} discovered={} fetched={} deleted={} structures={}",
response.getType(), hasEot, hasEcgMarkers, discoveredEcgCount, fetchedEcgCount, deletedEcgCount, describeStructures(structures));
if (recordKey != null && isNewRecordKey(recordKey)) {
seenRecordKeys.add(recordKey.getRawPayload());
final long timestampMs = recordKey.getTimestampMs();
@@ -257,8 +264,42 @@ public class WithingsEcgHandler implements ResponseHandler {
}
if (!waveformFetchQueued && hasEcgMarkers && discoveryPagesSeen < MAX_DISCOVERY_PAGES) {
queueDiscoveryProbe(false);
} else if (!waveformFetchQueued && hasEcgMarkers) {
logger.warn("Withings ECG discovery stopped after {} pages without queueing waveform fetch", discoveryPagesSeen);
}
} else if (!hasEot) {
logger.debug("Withings ECG discovery awaiting more packets for response type={}", response.getType());
}
}
private String describeStructures(final List<WithingsStructure> structures) {
final List<String> descriptions = new ArrayList<>(structures.size());
for (final WithingsStructure structure : structures) {
if (structure instanceof StoredMeasureMeta) {
final StoredMeasureMeta meta = (StoredMeasureMeta) structure;
descriptions.add("0116(type=" + meta.getMeasurementType() + ",ts=" + meta.getTimestampMs() + ")");
} else if (structure instanceof StoredMeasureData) {
final StoredMeasureData data = (StoredMeasureData) structure;
descriptions.add("0117(type=" + data.getMeasurementType() + ",raw=" + data.getRawValue() + ")");
} else if (structure instanceof StoredMeasureDataExtend) {
final StoredMeasureDataExtend dataExt = (StoredMeasureDataExtend) structure;
descriptions.add("0149(type=" + dataExt.getMeasurementType() + ",extra=" + dataExt.getExtraData() + ")");
} else if (structure instanceof StoredSignalMeta) {
final StoredSignalMeta meta = (StoredSignalMeta) structure;
descriptions.add("0143(signalType=" + meta.getSignalType() + ",flags=" + meta.getSignalFlags() + ",cursor=" + meta.getCursor() + ")");
} else if (structure instanceof StoredSignalMetaExtended) {
final byte[] rawPayload = ((StoredSignalMetaExtended) structure).getRawPayload();
descriptions.add("0146(" + GB.hexdump(rawPayload) + ")");
} else if (structure instanceof StoredSignalData) {
descriptions.add("0144(samples)");
} else if (structure instanceof RawWithingsStructure) {
final RawWithingsStructure raw = (RawWithingsStructure) structure;
descriptions.add(String.format("0x%04x(raw=%s)", raw.getType() & 0xffff, GB.hexdump(raw.getRawData())));
} else {
descriptions.add(String.format("0x%04x", structure.getType() & 0xffff));
}
}
return descriptions.toString();
}
private int computeEcgProgressPercent() {
@@ -425,9 +466,19 @@ public class WithingsEcgHandler implements ResponseHandler {
}
if (!support.hasEndOfTransmission(response)) {
logger.debug("Withings ECG waveform response awaiting more packets for ts={} structures={}",
startTimestampMs > 0 ? startTimestampMs : requestedRecordKey.getTimestampMs(),
describeStructures(structures));
return;
}
logger.debug("Withings ECG waveform response reached EOT for ts={} deleteKeyPresent={} verifyDeletion={} samples={} structures={}",
startTimestampMs > 0 ? startTimestampMs : requestedRecordKey.getTimestampMs(),
deleteKey != null,
verifyDeletion,
waveform.size(),
describeStructures(structures));
if (startTimestampMs <= 0) {
startTimestampMs = requestedRecordKey.getTimestampMs();
}
@@ -508,35 +559,6 @@ public class WithingsEcgHandler implements ResponseHandler {
}
}
private String describeStructures(final List<WithingsStructure> structures) {
final List<String> descriptions = new ArrayList<>(structures.size());
for (final WithingsStructure structure : structures) {
if (structure instanceof StoredMeasureMeta) {
final StoredMeasureMeta meta = (StoredMeasureMeta) structure;
descriptions.add("0116(type=" + meta.getMeasurementType() + ",ts=" + meta.getTimestampMs() + ")");
} else if (structure instanceof StoredMeasureData) {
final StoredMeasureData data = (StoredMeasureData) structure;
descriptions.add("0117(type=" + data.getMeasurementType() + ",raw=" + data.getRawValue() + ")");
} else if (structure instanceof StoredMeasureDataExtend) {
final StoredMeasureDataExtend dataExt = (StoredMeasureDataExtend) structure;
descriptions.add("0149(type=" + dataExt.getMeasurementType() + ",extra=" + dataExt.getExtraData() + ")");
} else if (structure instanceof StoredSignalMeta) {
final StoredSignalMeta meta = (StoredSignalMeta) structure;
descriptions.add("0143(signalType=" + meta.getSignalType() + ",flags=" + meta.getSignalFlags() + ",cursor=" + meta.getCursor() + ")");
} else if (structure instanceof StoredSignalMetaExtended) {
final byte[] rawPayload = ((StoredSignalMetaExtended) structure).getRawPayload();
descriptions.add("0146(" + GB.hexdump(rawPayload) + ")");
} else if (structure instanceof StoredSignalData) {
descriptions.add("0144(samples)");
} else if (structure instanceof RawWithingsStructure) {
final RawWithingsStructure raw = (RawWithingsStructure) structure;
descriptions.add(String.format("0x%04x(raw=%s)", raw.getType() & 0xffff, GB.hexdump(raw.getRawData())));
} else {
descriptions.add(String.format("0x%04x", structure.getType() & 0xffff));
}
}
return descriptions.toString();
}
}
private void storeWaveform(final long start,