AeroToss: UDP broadcast discovery + mDNS A record + имена
- Добавлен UDP broadcast discovery (порт 9999) как fallback - Desktop отправляет broadcast каждые 3 секунды - Android использует mDNS + UDP broadcast + Wi-Fi Direct - mDNS: добавлена A-запись для разрешения IP - Имена: Desktop=user@host, Android=manufacturer model - TXT record: id, name, port, type для определения устройства - Автоперерегистрация mDNS каждые 30 секунд - mDNS: отправка query каждые 5 секунд
This commit is contained in:
@@ -18,6 +18,8 @@ class CompositeAndroidDiscovery(
|
||||
private val _devices = MutableStateFlow<List<Device>>(emptyList())
|
||||
override val devices: Flow<List<Device>> = _devices.asStateFlow()
|
||||
|
||||
private val udpDiscovery = UdpBroadcastDiscovery()
|
||||
|
||||
@Volatile private var scope: CoroutineScope? = null
|
||||
@Volatile private var discoveryJob: kotlinx.coroutines.Job? = null
|
||||
|
||||
@@ -25,6 +27,7 @@ class CompositeAndroidDiscovery(
|
||||
if (discoveryJob != null) return
|
||||
|
||||
nsdDiscovery.startDiscovery(servicePort)
|
||||
udpDiscovery.startDiscovery(servicePort)
|
||||
wifiDirectManager.initialize()
|
||||
wifiDirectManager.discoverPeers()
|
||||
|
||||
@@ -32,10 +35,12 @@ class CompositeAndroidDiscovery(
|
||||
discoveryJob = scope?.launch {
|
||||
combine(
|
||||
nsdDiscovery.devices,
|
||||
udpDiscovery.devices,
|
||||
wifiDirectManager.devices
|
||||
) { nsdDevices, wifiDirectDevices ->
|
||||
) { nsdDevices, udpDevices, wifiDirectDevices ->
|
||||
val allDevices = mutableMapOf<String, Device>()
|
||||
nsdDevices.forEach { allDevices["nsd:${it.id}"] = it }
|
||||
udpDevices.forEach { allDevices["udp:${it.id}"] = it }
|
||||
wifiDirectDevices.forEach { allDevices["wifi:${it.id}"] = it }
|
||||
allDevices.values.toList()
|
||||
}.collect { devices ->
|
||||
@@ -48,6 +53,7 @@ class CompositeAndroidDiscovery(
|
||||
discoveryJob?.cancel()
|
||||
discoveryJob = null
|
||||
nsdDiscovery.stopDiscovery()
|
||||
udpDiscovery.stopDiscovery()
|
||||
wifiDirectManager.stopDiscovery()
|
||||
_devices.value = emptyList()
|
||||
}
|
||||
@@ -57,6 +63,7 @@ class CompositeAndroidDiscovery(
|
||||
scope?.cancel()
|
||||
scope = null
|
||||
nsdDiscovery.dispose()
|
||||
udpDiscovery.dispose()
|
||||
wifiDirectManager.cleanup()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.aerotoss.discovery
|
||||
|
||||
import com.aerotoss.model.Device
|
||||
import com.aerotoss.model.DeviceType
|
||||
import com.aerotoss.util.generateDeviceId
|
||||
import com.aerotoss.util.getDeviceName
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class UdpBroadcastDiscovery : DiscoveryManager {
|
||||
private val _devices = MutableStateFlow<List<Device>>(emptyList())
|
||||
override val devices: Flow<List<Device>> = _devices.asStateFlow()
|
||||
|
||||
private var serverSocket: DatagramSocket? = null
|
||||
private var discoveryThread: Thread? = null
|
||||
private val deviceId = generateDeviceId()
|
||||
private val deviceName = getDeviceName()
|
||||
private val discoveredDevices = ConcurrentHashMap<String, Device>()
|
||||
|
||||
override fun startDiscovery(servicePort: Int) {
|
||||
if (discoveryThread != null) return
|
||||
|
||||
discoveryThread = Thread {
|
||||
try {
|
||||
val socket = DatagramSocket(BROADCAST_PORT, InetAddress.getByName("0.0.0.0"))
|
||||
socket.broadcast = true
|
||||
socket.soTimeout = 2000
|
||||
serverSocket = socket
|
||||
|
||||
println("AeroToss-UDP: Listening on port $BROADCAST_PORT")
|
||||
|
||||
// Send discovery broadcast every 3 seconds
|
||||
var lastBroadcast = System.currentTimeMillis()
|
||||
|
||||
while (!Thread.currentThread().isInterrupted) {
|
||||
// Send broadcast
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastBroadcast > 3_000) {
|
||||
sendBroadcast(socket, servicePort)
|
||||
lastBroadcast = now
|
||||
}
|
||||
|
||||
// Listen for responses
|
||||
val buf = ByteArray(1024)
|
||||
try {
|
||||
val packet = DatagramPacket(buf, buf.size)
|
||||
socket.receive(packet)
|
||||
processPacket(packet, servicePort)
|
||||
} catch (_: java.net.SocketTimeoutException) {
|
||||
}
|
||||
}
|
||||
socket.close()
|
||||
} catch (e: Exception) {
|
||||
if (!Thread.currentThread().isInterrupted) {
|
||||
println("AeroToss-UDP: Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
isDaemon = true
|
||||
name = "aerotoss-udp"
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendBroadcast(socket: DatagramSocket, servicePort: Int) {
|
||||
try {
|
||||
val message = "AEROTOSS:$servicePort:$deviceId:$deviceName"
|
||||
val data = message.toByteArray(Charsets.UTF_8)
|
||||
val broadcastAddr = InetAddress.getByName("255.255.255.255")
|
||||
val packet = DatagramPacket(data, data.size, broadcastAddr, BROADCAST_PORT)
|
||||
socket.send(packet)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun processPacket(packet: DatagramPacket, myPort: Int) {
|
||||
try {
|
||||
val message = String(packet.data, 0, packet.length, Charsets.UTF_8)
|
||||
if (!message.startsWith("AEROTOSS:")) return
|
||||
|
||||
val parts = message.split(":", limit = 4)
|
||||
if (parts.size < 4) return
|
||||
|
||||
val remotePort = parts[1].toIntOrNull() ?: return
|
||||
val remoteId = parts[2]
|
||||
val remoteName = parts[3]
|
||||
|
||||
// Don't add ourselves
|
||||
if (remoteId == deviceId) return
|
||||
if (remotePort == myPort) return
|
||||
|
||||
val hostAddress = packet.address.hostAddress ?: return
|
||||
|
||||
val device = Device(
|
||||
id = remoteId,
|
||||
name = remoteName,
|
||||
type = if (remotePort == myPort) DeviceType.DESKTOP else DeviceType.PHONE,
|
||||
hostAddress = hostAddress,
|
||||
port = remotePort
|
||||
)
|
||||
|
||||
discoveredDevices[remoteId] = device
|
||||
_devices.value = discoveredDevices.values.toList()
|
||||
println("AeroToss-UDP: Found $remoteName at $hostAddress:$remotePort")
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
override fun stopDiscovery() {
|
||||
discoveryThread?.interrupt()
|
||||
discoveryThread = null
|
||||
try { serverSocket?.close() } catch (_: Exception) {}
|
||||
serverSocket = null
|
||||
discoveredDevices.clear()
|
||||
_devices.value = emptyList()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
stopDiscovery()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BROADCAST_PORT = 9999
|
||||
}
|
||||
}
|
||||
@@ -230,13 +230,16 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
}
|
||||
|
||||
private fun buildDnsResponse(servicePort: Int): ByteArray {
|
||||
val localAddr = findLocalAddress()
|
||||
val localIp = localAddr?.address ?: byteArrayOf(127, 0, 0, 1)
|
||||
|
||||
val baos = java.io.ByteArrayOutputStream()
|
||||
val dos = java.io.DataOutputStream(baos)
|
||||
|
||||
dos.writeShort(0) // ID
|
||||
dos.writeShort(0x8400) // Flags
|
||||
dos.writeShort(0) // QDCOUNT
|
||||
dos.writeShort(3) // ANCOUNT
|
||||
dos.writeShort(4) // ANCOUNT (PTR + SRV + TXT + A)
|
||||
dos.writeShort(0) // NSCOUNT
|
||||
dos.writeShort(0) // ARCOUNT
|
||||
|
||||
@@ -287,6 +290,14 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
dos.writeShort(txtBytes.size)
|
||||
dos.write(txtBytes)
|
||||
|
||||
// A: AeroToss._aerotoss._tcp.local. → IP address
|
||||
writeDnsName(baos, "AeroToss._aerotoss._tcp.local.")
|
||||
dos.writeShort(1) // A
|
||||
dos.writeShort(1) // IN
|
||||
dos.writeInt(4500) // TTL
|
||||
dos.writeShort(4) // RDLENGTH = 4 bytes for IPv4
|
||||
dos.write(localIp)
|
||||
|
||||
dos.flush()
|
||||
return baos.toByteArray()
|
||||
}
|
||||
@@ -352,4 +363,19 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
const val SERVICE_TYPE = "_aerotoss._tcp"
|
||||
const val SERVICE_NAME = "AeroToss"
|
||||
}
|
||||
|
||||
private fun findLocalAddress(): InetAddress? {
|
||||
return try {
|
||||
NetworkInterface.getNetworkInterfaces()?.toList()
|
||||
?.filter { iface ->
|
||||
iface.isUp && !iface.isLoopback && !isVirtual(iface) &&
|
||||
iface.inetAddresses.toList().any { it is java.net.Inet4Address }
|
||||
}
|
||||
?.flatMap { it.inetAddresses.toList() }
|
||||
?.firstOrNull { it is java.net.Inet4Address }
|
||||
?: InetAddress.getLocalHost()
|
||||
} catch (_: Exception) {
|
||||
try { InetAddress.getLocalHost() } catch (_: Exception) { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user