(the text below and suggestions were written by AI based on my input from using the app and code v4.0.0)
Some context on what I'm actually trying to set up, since it explains the scenario I keep hitting: at home both devices sit on the same Wi-Fi, and that works well. When I'm away from home I'd like the phone to fall back to BLE, and then pick Wi-Fi back up when I get home. So I've been switching Wi-Fi off and on a lot, which seems to be a good way to run into the connection drops you mentioned you couldn't reproduce.
I haven't captured logs — everything below is from reading the source (v4.0.0) and watching the text of the persistent notification on the phone. So please treat all of it as a guess rather than a diagnosis. Device is a Pixel 10 Pro, AirSync set to "Unrestricted" battery usage, auto-reconnect on, Nearby/BLE on, Mac-side Nearby + auto-connect + auto-switch all on.
What I observed
- Both devices on the same Wi-Fi, connected normally.
- Wi-Fi switched off on the phone → BLE connects automatically. Good.
- BLE stays up reliably for as long as the phone's screen is on.
- Screen off → BLE disconnects within about a minute.
- Screen back on → BLE does not come back on its own.
- Wi-Fi switched back on, phone joins the same network → notification goes
Reconnecting... → Connecting... → No device connected, and it never connects. A manual connect from the phone fixes it.
Guess 1 — the notification sequence in step 6 seems to say the loop cancels mid-attempt
If I read AirSyncService.buildNotification() correctly, the title Connecting... only renders when isAuto == false and isConnecting == true:
} else if (isAuto) -> "Reconnecting..."
} else if (isConnecting) -> "Connecting..."
} else -> "No device connected"
So going Reconnecting... → Connecting... looks like the auto-reconnect loop was cancelled while a connection attempt was still in flight. And inside the loop the only cancel condition is:
if (manual || !autoEnabled) { cancelAutoReconnect(); break }
Auto-reconnect is on, so that would mean userManuallyDisconnected became true during the reconnect — even though I never touched a disconnect button.
The only remote path I can find that sets it is:
"disconnectRequest" -> handleDisconnectRequest(context) // → setUserManuallyDisconnected(true)
And on the Mac side sendDisconnectRequest() is called from AppState.disconnectDevice(), which isn't only the UI action — I see it called from WebSocketServer+Ping.swift:108 (ping timeout) and WebSocketServer.swift:258 (socket disconnect callback) as well as AppDelegate.
So my guess is something like: phone's Wi-Fi goes away → Mac's ping times out → Mac calls disconnectDevice() and sends disconnectRequest → that message reaches the phone late, or lands on the freshly re-established session → the phone records it as a user disconnect and stops trying.
If that's roughly right, the interesting part isn't the timing but the semantics: a disconnectRequest caused by a network drop ends up indistinguishable from "the user pressed disconnect". Would it make sense for the two to be tracked separately, so a link-loss disconnect doesn't suppress auto-reconnect?
Guess 2 — it seems to only clear on a manual connect from the phone
This is what made me notice guess 1 at all. Earlier in my testing BLE wouldn't auto-connect either, and connecting manually from the Mac didn't change anything. What fixed it was connecting manually from the phone once — after that, BLE started auto-connecting on Wi-Fi loss reliably.
Looking at where the flag is cleared, that seems to fit: connect(manualAttempt = true), WakeupHandler, the QS tile's connect branch and the widget. A Mac-initiated BLE connection doesn't appear to go through any of them.
If that's accurate, a user who lands in this state has no way to tell — everything looks enabled, nothing reports an error, the automatic behaviour is just silently off until they happen to connect manually from the phone. That might be one reason these reports are hard to reproduce: your own flag is presumably clean.
Guess 3 — BLE dropping on screen-off
For step 4, BleGattServer.startHeartbeat() is a plain coroutine:
while (isActive && isAuthenticated) { delay(5000); sendNotification(CHAR_BATTERY_LEVEL, ...) }
and the Mac disconnects after 25 s without traffic (resetWatchdog()). delay() won't wake a suspended CPU, and I couldn't find a newWakeLock anywhere in the Android repo, so my guess is those 5 s ticks slip past 25 s once the SoC suspends. "Unrestricted" battery didn't help, which would fit — it exempts the app from Doze restrictions but doesn't stop the CPU suspending.
It's also consistent with Wi-Fi surviving screen-off while BLE doesn't: over Wi-Fi the Mac's packets arrive and wake the phone, whereas the BLE heartbeat has to be initiated by the phone, so nothing wakes it.
I'd suggest not fixing this with a partial wake lock — holding one across a whole BLE session would likely cost far more battery than the 10 s reconnect loop you measured at under 1%/day. Two directions that seem cheaper:
- Invert the heartbeat: have the Mac write to a characteristic on an interval instead. An incoming BLE write wakes the phone's CPU by itself, so the phone doesn't need to stay awake or schedule anything. It's the same idea as your "Active Burst, Passive Listen" for Wi-Fi discovery — active side on the machine that's plugged in.
- Or lean on BLE's own link supervision and relax the app-level watchdog. CoreBluetooth reports
didDisconnectPeripheral when the link actually drops, so a 25 s application timeout may be doing work the protocol already does.
I don't know which fits your architecture better.
Summary
- Guess 1 and 2 are the ones I'd look at first, since together they'd leave a user permanently without automatic reconnection with no visible symptom, which sounds like the shape of the reports you can't reproduce.
- Guess 3 is more contained and I'm fairly confident about the mechanism, less so about the right fix.
All of this is inference from reading code plus notification text, so I may have misread any of it.
Thank you.
(the text below and suggestions were written by AI based on my input from using the app and code v4.0.0)
Some context on what I'm actually trying to set up, since it explains the scenario I keep hitting: at home both devices sit on the same Wi-Fi, and that works well. When I'm away from home I'd like the phone to fall back to BLE, and then pick Wi-Fi back up when I get home. So I've been switching Wi-Fi off and on a lot, which seems to be a good way to run into the connection drops you mentioned you couldn't reproduce.
I haven't captured logs — everything below is from reading the source (v4.0.0) and watching the text of the persistent notification on the phone. So please treat all of it as a guess rather than a diagnosis. Device is a Pixel 10 Pro, AirSync set to "Unrestricted" battery usage, auto-reconnect on, Nearby/BLE on, Mac-side Nearby + auto-connect + auto-switch all on.
What I observed
Reconnecting...→Connecting...→No device connected, and it never connects. A manual connect from the phone fixes it.Guess 1 — the notification sequence in step 6 seems to say the loop cancels mid-attempt
If I read
AirSyncService.buildNotification()correctly, the titleConnecting...only renders whenisAuto == falseandisConnecting == true:So going
Reconnecting...→Connecting...looks like the auto-reconnect loop was cancelled while a connection attempt was still in flight. And inside the loop the only cancel condition is:Auto-reconnect is on, so that would mean
userManuallyDisconnectedbecametrueduring the reconnect — even though I never touched a disconnect button.The only remote path I can find that sets it is:
And on the Mac side
sendDisconnectRequest()is called fromAppState.disconnectDevice(), which isn't only the UI action — I see it called fromWebSocketServer+Ping.swift:108(ping timeout) andWebSocketServer.swift:258(socket disconnect callback) as well asAppDelegate.So my guess is something like: phone's Wi-Fi goes away → Mac's ping times out → Mac calls
disconnectDevice()and sendsdisconnectRequest→ that message reaches the phone late, or lands on the freshly re-established session → the phone records it as a user disconnect and stops trying.If that's roughly right, the interesting part isn't the timing but the semantics: a
disconnectRequestcaused by a network drop ends up indistinguishable from "the user pressed disconnect". Would it make sense for the two to be tracked separately, so a link-loss disconnect doesn't suppress auto-reconnect?Guess 2 — it seems to only clear on a manual connect from the phone
This is what made me notice guess 1 at all. Earlier in my testing BLE wouldn't auto-connect either, and connecting manually from the Mac didn't change anything. What fixed it was connecting manually from the phone once — after that, BLE started auto-connecting on Wi-Fi loss reliably.
Looking at where the flag is cleared, that seems to fit:
connect(manualAttempt = true),WakeupHandler, the QS tile's connect branch and the widget. A Mac-initiated BLE connection doesn't appear to go through any of them.If that's accurate, a user who lands in this state has no way to tell — everything looks enabled, nothing reports an error, the automatic behaviour is just silently off until they happen to connect manually from the phone. That might be one reason these reports are hard to reproduce: your own flag is presumably clean.
Guess 3 — BLE dropping on screen-off
For step 4,
BleGattServer.startHeartbeat()is a plain coroutine:and the Mac disconnects after 25 s without traffic (
resetWatchdog()).delay()won't wake a suspended CPU, and I couldn't find anewWakeLockanywhere in the Android repo, so my guess is those 5 s ticks slip past 25 s once the SoC suspends. "Unrestricted" battery didn't help, which would fit — it exempts the app from Doze restrictions but doesn't stop the CPU suspending.It's also consistent with Wi-Fi surviving screen-off while BLE doesn't: over Wi-Fi the Mac's packets arrive and wake the phone, whereas the BLE heartbeat has to be initiated by the phone, so nothing wakes it.
I'd suggest not fixing this with a partial wake lock — holding one across a whole BLE session would likely cost far more battery than the 10 s reconnect loop you measured at under 1%/day. Two directions that seem cheaper:
didDisconnectPeripheralwhen the link actually drops, so a 25 s application timeout may be doing work the protocol already does.I don't know which fits your architecture better.
Summary
All of this is inference from reading code plus notification text, so I may have misread any of it.
Thank you.