feat(clients): Wake-on-LAN in apple/linux/windows/android/decky
apple / swift (push) Successful in 1m7s
audit / cargo-audit (push) Successful in 1m14s
ci / rust (push) Failing after 49s
ci / web (push) Successful in 52s
windows-host / package (push) Failing after 2m58s
ci / docs-site (push) Successful in 1m5s
android / android (push) Successful in 4m7s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m15s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m15s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 48s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 49s
ci / bench (push) Successful in 5m5s
decky / build-publish (push) Successful in 29s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
release / apple (push) Successful in 8m30s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
apple / screenshots (push) Has been cancelled
docker / deploy-docs (push) Successful in 19s
apple / swift (push) Successful in 1m7s
audit / cargo-audit (push) Successful in 1m14s
ci / rust (push) Failing after 49s
ci / web (push) Successful in 52s
windows-host / package (push) Failing after 2m58s
ci / docs-site (push) Successful in 1m5s
android / android (push) Successful in 4m7s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m15s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m15s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 48s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 49s
ci / bench (push) Successful in 5m5s
decky / build-publish (push) Successful in 29s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
release / apple (push) Successful in 8m30s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
apple / screenshots (push) Has been cancelled
docker / deploy-docs (push) Successful in 19s
Each client learns a host's MAC from the mDNS `mac` TXT while it's awake, persists it on the saved-host record, and — when reconnecting to an offline host — sends a magic packet before connecting, plus an explicit "Wake host" action. Apple wraps the C-ABI; linux/windows call the core fn directly (linux also gains a --wake CLI mode); android via a new nativeWakeOnLan JNI export (the mDNS browse record gains a 7th mac field); decky shells out to the linux client's --wake before launching the stream. iOS/tvOS need the managed com.apple.developer.networking.multicast entitlement (pending Apple approval), so the wake path + UI are gated off via PunktfunkConnection.wakeOnLANAvailable and the entitlement is commented out — keeping iOS/tvOS releasable. MAC-learning stays active on every platform so it lights up the moment it's ungated. macOS works today. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,25 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) {
|
||||
val identityStore = remember { IdentityStore(context) }
|
||||
val knownHostStore = remember { KnownHostStore(context) }
|
||||
var savedHosts by remember { mutableStateOf(knownHostStore.all()) }
|
||||
// Learn wake MAC(s) from live adverts for hosts we've saved (parity with the desktop clients),
|
||||
// so we can Wake-on-LAN them once they sleep. Runs only when the discovered set changes; the
|
||||
// prefs write is guarded (no-op when unchanged), and we refresh the saved list only if a MAC
|
||||
// was actually newly learned.
|
||||
LaunchedEffect(discovered) {
|
||||
val learned = withContext(Dispatchers.IO) {
|
||||
var any = false
|
||||
discovered.forEach { dh ->
|
||||
if (dh.mac.isNotEmpty() &&
|
||||
knownHostStore.get(dh.host, dh.port)?.let { it.mac != dh.mac } == true
|
||||
) {
|
||||
knownHostStore.learnMac(dh.host, dh.port, dh.mac)
|
||||
any = true
|
||||
}
|
||||
}
|
||||
any
|
||||
}
|
||||
if (learned) savedHosts = knownHostStore.all()
|
||||
}
|
||||
// Mint-once on genuine first run; an Unrecoverable store (decrypt failure) surfaces here and
|
||||
// refuses to connect — never silently shadow-minting a new identity (which would force re-pair).
|
||||
var identity by remember { mutableStateOf<ClientIdentity?>(null) }
|
||||
@@ -176,6 +195,14 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) {
|
||||
}
|
||||
connecting = true
|
||||
status = "Connecting to $targetHost:$targetPort…"
|
||||
// Auto-wake: reconnecting to a saved host that may be asleep. If we learned its MAC while it
|
||||
// was online and it isn't currently advertising, fire a magic packet first — the connect's
|
||||
// own timeout gives a woken host time to come up (harmless if it's already awake).
|
||||
knownHostStore.get(targetHost, targetPort)?.mac
|
||||
?.takeIf { it.isNotEmpty() && discovered.none { d -> d.host == targetHost && d.port == targetPort } }
|
||||
?.let { macs ->
|
||||
scope.launch(Dispatchers.IO) { NativeBridge.nativeWakeOnLan(macs.joinToString(","), targetHost) }
|
||||
}
|
||||
discovery.stop() // free the Wi-Fi radio before the stream session
|
||||
scope.launch {
|
||||
val handle = connectNative(id, targetHost, targetPort, pinHex ?: "", CONNECT_TIMEOUT_MS)
|
||||
@@ -359,6 +386,15 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) {
|
||||
savedHosts = knownHostStore.all()
|
||||
},
|
||||
onRename = { renameTarget = kh },
|
||||
// Explicit wake: offered only when the host is offline and we have a MAC to
|
||||
// target (a tap-to-connect already auto-wakes an offline saved host).
|
||||
onWake = if (kh.mac.isNotEmpty() &&
|
||||
discovered.none { it.host == kh.address && it.port == kh.port }
|
||||
) {
|
||||
{ scope.launch(Dispatchers.IO) { NativeBridge.nativeWakeOnLan(kh.mac.joinToString(","), kh.address) } }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ fun HostCard(
|
||||
onConnect: () -> Unit,
|
||||
onForget: (() -> Unit)?,
|
||||
onRename: (() -> Unit)? = null,
|
||||
onWake: (() -> Unit)? = null,
|
||||
) {
|
||||
// D-pad / controller focus highlight: a clickable card is focusable, but the default state
|
||||
// layer is too subtle on a TV across a room — draw a clear primary-colour border when focused.
|
||||
@@ -107,7 +108,7 @@ fun HostCard(
|
||||
StatusPill(status)
|
||||
}
|
||||
|
||||
if (onForget != null || onRename != null) {
|
||||
if (onForget != null || onRename != null || onWake != null) {
|
||||
var menu by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.align(Alignment.TopEnd)) {
|
||||
IconButton(enabled = enabled, onClick = { menu = true }) {
|
||||
@@ -119,6 +120,15 @@ fun HostCard(
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) {
|
||||
if (onWake != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Wake host") },
|
||||
onClick = {
|
||||
menu = false
|
||||
onWake()
|
||||
},
|
||||
)
|
||||
}
|
||||
if (onRename != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Rename") },
|
||||
|
||||
Reference in New Issue
Block a user