AeroToss: mDNS queries + имена устройств + типы
- Desktop отправляет mDNS-запросы каждые 5 секунд для обнаружения - При обнаружении читается TXT record: name, id, type, port - Тип устройства определяется из TXT (phone/desktop) - Android NSD: setAttribute для TXT записей (id, name, port, type) - Имена: Desktop = user@hostname, Android = manufacturer model - Исправлен тип устройства: DESKTOP → PHONE/DESKTOP из TXT
This commit is contained in:
@@ -36,20 +36,18 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
return@Thread
|
||||
}
|
||||
|
||||
// Try each interface until we find one that works
|
||||
for (iface in interfaces) {
|
||||
try {
|
||||
println("AeroToss: Trying interface ${iface.name} (${iface.inetAddresses.toList().map { it.hostAddress }})")
|
||||
println("AeroToss: Trying ${iface.name} (${getIpv4(iface)?.hostAddress})")
|
||||
runOnInterface(iface, servicePort)
|
||||
break // If successful, stay on this interface
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
println("AeroToss: Interface ${iface.name} failed: ${e.message}")
|
||||
println("AeroToss: ${iface.name} failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (!Thread.currentThread().isInterrupted) {
|
||||
e.printStackTrace()
|
||||
println("AeroToss: mDNS error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
@@ -69,12 +67,11 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
|
||||
println("AeroToss: Listening on ${iface.name}")
|
||||
|
||||
// Initial registration
|
||||
registerService(socket, group, servicePort)
|
||||
|
||||
// Listen for queries and respond
|
||||
val buf = ByteArray(4096)
|
||||
var lastRegister = System.currentTimeMillis()
|
||||
var lastQuery = System.currentTimeMillis()
|
||||
|
||||
while (!Thread.currentThread().isInterrupted) {
|
||||
val packet = DatagramPacket(buf, buf.size)
|
||||
@@ -83,24 +80,57 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
socket.receive(packet)
|
||||
processPacket(packet.data, packet.length, packet.address)
|
||||
} catch (_: java.net.SocketTimeoutException) {
|
||||
// Re-register periodically (every 30 seconds)
|
||||
val now = System.currentTimeMillis()
|
||||
// Re-register every 30s
|
||||
if (now - lastRegister > 30_000) {
|
||||
registerService(socket, group, servicePort)
|
||||
lastRegister = now
|
||||
}
|
||||
// Send discovery query every 5s
|
||||
if (now - lastQuery > 5_000) {
|
||||
sendQuery(socket, group)
|
||||
lastQuery = now
|
||||
}
|
||||
}
|
||||
}
|
||||
socket.leaveGroup(group)
|
||||
socket.close()
|
||||
}
|
||||
|
||||
private fun sendQuery(socket: MulticastSocket, group: InetAddress) {
|
||||
try {
|
||||
val query = buildDnsQuery()
|
||||
val packet = DatagramPacket(query, query.size, group, 5353)
|
||||
socket.send(packet)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun buildDnsQuery(): ByteArray {
|
||||
val baos = java.io.ByteArrayOutputStream()
|
||||
val dos = java.io.DataOutputStream(baos)
|
||||
|
||||
dos.writeShort(0) // ID
|
||||
dos.writeShort(0x0000) // Flags: standard query
|
||||
dos.writeShort(1) // QDCOUNT
|
||||
dos.writeShort(0) // ANCOUNT
|
||||
dos.writeShort(0) // NSCOUNT
|
||||
dos.writeShort(0) // ARCOUNT
|
||||
|
||||
// Question: _aerotoss._tcp.local. type=ANY class=IN
|
||||
writeDnsName(baos, "_aerotoss._tcp.local.")
|
||||
dos.writeShort(255) // type ANY
|
||||
dos.writeShort(1) // class IN
|
||||
|
||||
dos.flush()
|
||||
return baos.toByteArray()
|
||||
}
|
||||
|
||||
private fun registerService(socket: MulticastSocket, group: InetAddress, servicePort: Int) {
|
||||
val response = buildDnsResponse(servicePort)
|
||||
val packet = DatagramPacket(response, response.size, group, 5353)
|
||||
try {
|
||||
socket.send(packet)
|
||||
println("AeroToss: Registered service on port $servicePort")
|
||||
println("AeroToss: Registered on port $servicePort as '$deviceName'")
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
@@ -108,12 +138,17 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
if (length < 12) return
|
||||
|
||||
val qr = (data[2].toInt() shr 7) and 1
|
||||
if (qr != 1) return // Only process responses
|
||||
if (qr != 1) return
|
||||
|
||||
val anCount = ((data[6].toInt() and 0xFF) shl 8) or (data[7].toInt() and 0xFF)
|
||||
if (anCount == 0) return
|
||||
|
||||
var offset = 12
|
||||
var foundSrv = false
|
||||
var foundTxt = false
|
||||
var srvPort = 0
|
||||
var txtAttributes = mapOf<String, String>()
|
||||
|
||||
for (i in 0 until anCount) {
|
||||
offset = skipName(data, offset, length)
|
||||
if (offset + 10 > length) return
|
||||
@@ -127,41 +162,43 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
if (type == 33 && offset + rdLength <= length) {
|
||||
// SRV record
|
||||
val srvData = data.copyOfRange(offset, offset + rdLength)
|
||||
val port = ((srvData[2].toInt() and 0xFF) shl 8) or (srvData[3].toInt() and 0xFF)
|
||||
|
||||
val key = fromAddress.hostAddress ?: continue
|
||||
if (!discoveredDevices.containsKey(key)) {
|
||||
val device = Device(
|
||||
id = key,
|
||||
name = "Device@${fromAddress.hostAddress}",
|
||||
type = DeviceType.PHONE,
|
||||
hostAddress = fromAddress.hostAddress,
|
||||
port = port
|
||||
)
|
||||
discoveredDevices[key] = device
|
||||
_devices.value = discoveredDevices.values.toList()
|
||||
println("AeroToss: Discovered device at ${fromAddress.hostAddress}:$port")
|
||||
}
|
||||
srvPort = ((srvData[2].toInt() and 0xFF) shl 8) or (srvData[3].toInt() and 0xFF)
|
||||
foundSrv = true
|
||||
}
|
||||
|
||||
if (type == 16 && offset + rdLength <= length) {
|
||||
// TXT record
|
||||
val txtData = data.copyOfRange(offset, offset + rdLength)
|
||||
val attributes = parseTxtRecord(txtData)
|
||||
val name = attributes["name"] ?: "Device@${fromAddress.hostAddress}"
|
||||
val id = attributes["id"] ?: fromAddress.hostAddress
|
||||
|
||||
val key = fromAddress.hostAddress ?: continue
|
||||
val existing = discoveredDevices[key]
|
||||
if (existing != null && id != deviceId) {
|
||||
val updated = existing.copy(name = name, id = id)
|
||||
discoveredDevices[key] = updated
|
||||
_devices.value = discoveredDevices.values.toList()
|
||||
}
|
||||
txtAttributes = parseTxtRecord(txtData)
|
||||
foundTxt = true
|
||||
}
|
||||
|
||||
offset += rdLength
|
||||
}
|
||||
|
||||
if (foundSrv && foundTxt) {
|
||||
val key = fromAddress.hostAddress ?: return
|
||||
val id = txtAttributes["id"] ?: return
|
||||
val name = txtAttributes["name"] ?: fromAddress.hostAddress
|
||||
val port = txtAttributes["port"]?.toIntOrNull() ?: srvPort
|
||||
val deviceType = when (txtAttributes["type"]) {
|
||||
"phone" -> DeviceType.PHONE
|
||||
else -> DeviceType.DESKTOP
|
||||
}
|
||||
|
||||
if (id != deviceId) {
|
||||
val device = Device(
|
||||
id = id,
|
||||
name = name,
|
||||
type = deviceType,
|
||||
hostAddress = fromAddress.hostAddress,
|
||||
port = port
|
||||
)
|
||||
discoveredDevices[key] = device
|
||||
_devices.value = discoveredDevices.values.toList()
|
||||
println("AeroToss: Found $name at ${fromAddress.hostAddress}:$port")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun skipName(data: ByteArray, offset: Int, length: Int): Int {
|
||||
@@ -196,33 +233,32 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
val baos = java.io.ByteArrayOutputStream()
|
||||
val dos = java.io.DataOutputStream(baos)
|
||||
|
||||
// Header
|
||||
dos.writeShort(0) // ID
|
||||
dos.writeShort(0x8400) // Flags: response, authoritative
|
||||
dos.writeShort(0x8400) // Flags
|
||||
dos.writeShort(0) // QDCOUNT
|
||||
dos.writeShort(3) // ANCOUNT (PTR + SRV + TXT)
|
||||
dos.writeShort(3) // ANCOUNT
|
||||
dos.writeShort(0) // NSCOUNT
|
||||
dos.writeShort(0) // ARCOUNT
|
||||
|
||||
// PTR record
|
||||
// PTR: _aerotoss._tcp.local. → AeroToss._aerotoss._tcp.local.
|
||||
writeDnsName(baos, "_aerotoss._tcp.local.")
|
||||
dos.writeShort(12) // PTR
|
||||
dos.writeShort(1) // class IN
|
||||
dos.writeInt(120) // TTL
|
||||
dos.writeShort(1) // IN
|
||||
dos.writeInt(4500) // TTL
|
||||
val ptrRd = java.io.ByteArrayOutputStream()
|
||||
writeDnsName(ptrRd, "AeroToss._aerotoss._tcp.local.")
|
||||
val ptrBytes = ptrRd.toByteArray()
|
||||
dos.writeShort(ptrBytes.size)
|
||||
dos.write(ptrBytes)
|
||||
|
||||
// SRV record
|
||||
// SRV: AeroToss._aerotoss._tcp.local. → port
|
||||
writeDnsName(baos, "AeroToss._aerotoss._tcp.local.")
|
||||
dos.writeShort(33) // SRV
|
||||
dos.writeShort(1) // class IN
|
||||
dos.writeInt(120) // TTL
|
||||
dos.writeShort(1) // IN
|
||||
dos.writeInt(4500) // TTL
|
||||
val srvRd = java.io.ByteArrayOutputStream()
|
||||
srvRd.write(0) // priority
|
||||
srvRd.write(0) // weight
|
||||
srvRd.write(0); srvRd.write(0) // priority
|
||||
srvRd.write(0); srvRd.write(0) // weight
|
||||
srvRd.write((servicePort shr 8) and 0xFF)
|
||||
srvRd.write(servicePort and 0xFF)
|
||||
writeDnsName(srvRd, "AeroToss._aerotoss._tcp.local.")
|
||||
@@ -230,15 +266,23 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
dos.writeShort(srvBytes.size)
|
||||
dos.write(srvBytes)
|
||||
|
||||
// TXT record
|
||||
// TXT: AeroToss._aerotoss._tcp.local. → id, name, port
|
||||
writeDnsName(baos, "AeroToss._aerotoss._tcp.local.")
|
||||
dos.writeShort(16) // TXT
|
||||
dos.writeShort(1) // class IN
|
||||
dos.writeInt(120) // TTL
|
||||
dos.writeShort(1) // IN
|
||||
dos.writeInt(4500) // TTL
|
||||
val txtRd = java.io.ByteArrayOutputStream()
|
||||
val txtEntry = "id=$deviceId name=$deviceName port=$servicePort".toByteArray(Charsets.UTF_8)
|
||||
txtRd.write(txtEntry.size)
|
||||
txtRd.write(txtEntry)
|
||||
val entries = listOf(
|
||||
"id=$deviceId",
|
||||
"name=$deviceName",
|
||||
"port=$servicePort",
|
||||
"type=desktop"
|
||||
)
|
||||
for (entry in entries) {
|
||||
val bytes = entry.toByteArray(Charsets.UTF_8)
|
||||
txtRd.write(bytes.size)
|
||||
txtRd.write(bytes)
|
||||
}
|
||||
val txtBytes = txtRd.toByteArray()
|
||||
dos.writeShort(txtBytes.size)
|
||||
dos.write(txtBytes)
|
||||
@@ -256,6 +300,10 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
os.write(0)
|
||||
}
|
||||
|
||||
private fun getIpv4(iface: NetworkInterface): InetAddress? {
|
||||
return iface.inetAddresses.toList().firstOrNull { it is java.net.Inet4Address }
|
||||
}
|
||||
|
||||
private fun getAvailableInterfaces(): List<NetworkInterface> {
|
||||
return NetworkInterface.getNetworkInterfaces()?.toList()
|
||||
?.filter { iface ->
|
||||
@@ -264,9 +312,8 @@ class JmdnsDiscovery : DiscoveryManager {
|
||||
!isVirtual(iface) &&
|
||||
iface.inetAddresses.toList().any { it is java.net.Inet4Address }
|
||||
}
|
||||
?.sortedByDescending { iface: NetworkInterface ->
|
||||
// Prefer interfaces with private IP addresses (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
|
||||
iface.inetAddresses.toList().any { addr: java.net.InetAddress ->
|
||||
?.sortedByDescending { iface ->
|
||||
iface.inetAddresses.toList().any { addr ->
|
||||
addr is java.net.Inet4Address && isPrivateAddress(addr)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user