133 lines
3.8 KiB
JavaScript
133 lines
3.8 KiB
JavaScript
'use strict'
|
|
|
|
// UDP discovery for the LAN sync: beacons fill the devices list and double
|
|
// as liveness (a device drops off a few seconds after close).
|
|
|
|
const dgram = require('dgram')
|
|
const os = require('os')
|
|
|
|
// Broadcast to the global address AND to every interface's own subnet
|
|
// broadcast - some networks drop one or the other.
|
|
function broadcastAddresses() {
|
|
const out = ['255.255.255.255']
|
|
const interfaces = os.networkInterfaces()
|
|
for (const list of Object.values(interfaces)) {
|
|
for (const iface of list || []) {
|
|
if (iface.family !== 'IPv4' || iface.internal || !iface.netmask) continue
|
|
const addr = ipToInt(iface.address)
|
|
const mask = ipToInt(iface.netmask)
|
|
if (addr == null || mask == null) continue
|
|
out.push(intToIp((addr | (~mask >>> 0)) >>> 0))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
function ipToInt(ip) {
|
|
const parts = String(ip).split('.')
|
|
if (parts.length !== 4) return null
|
|
let out = 0
|
|
for (const part of parts) {
|
|
const n = Number(part)
|
|
if (!Number.isInteger(n) || n < 0 || n > 255) return null
|
|
out = (out << 8) | n
|
|
}
|
|
return out >>> 0
|
|
}
|
|
|
|
function intToIp(int) {
|
|
return [24, 16, 8, 0].map(shift => (int >>> shift) & 255).join('.')
|
|
}
|
|
|
|
function createDiscovery({ identity, tag, protocol, port, beaconMs, deviceTtlMs, getPort }) {
|
|
let socket = null
|
|
let timer = null
|
|
let devicesCb = null
|
|
// id -> { id, name, address, port, at }
|
|
const seen = new Map()
|
|
|
|
function listDevices() {
|
|
const known = identity().known
|
|
const now = Date.now()
|
|
return [...seen.values()]
|
|
.filter(d => now - d.at <= deviceTtlMs)
|
|
.map(d => {
|
|
const pair = known[d.id]
|
|
return { id: d.id, name: d.name, address: d.address, port: d.port, paired: !!pair, code: pair ? pair.code : '' }
|
|
})
|
|
.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
|
}
|
|
|
|
function emitDevices() {
|
|
if (devicesCb) devicesCb(listDevices())
|
|
}
|
|
|
|
function pruneDevices() {
|
|
const now = Date.now()
|
|
let changed = false
|
|
for (const [id, device] of seen) {
|
|
if (now - device.at > deviceTtlMs) {
|
|
seen.delete(id)
|
|
changed = true
|
|
}
|
|
}
|
|
if (changed) emitDevices()
|
|
}
|
|
|
|
function beacon() {
|
|
if (!socket || !getPort()) return
|
|
const me = identity()
|
|
const payload = Buffer.from(JSON.stringify({ t: tag, v: protocol, id: me.deviceId, name: os.hostname(), port: getPort() }))
|
|
for (const address of broadcastAddresses()) {
|
|
try {
|
|
socket.send(payload, 0, payload.length, port, address)
|
|
} catch {}
|
|
}
|
|
pruneDevices()
|
|
}
|
|
|
|
function start(onDevices) {
|
|
stop()
|
|
devicesCb = onDevices
|
|
socket = dgram.createSocket({ type: 'udp4', reuseAddr: true })
|
|
socket.on('error', () => stop())
|
|
socket.on('message', (buf, rinfo) => {
|
|
let msg = null
|
|
try {
|
|
msg = JSON.parse(buf.toString('utf8'))
|
|
} catch {}
|
|
if (!msg || msg.t !== tag || msg.v !== protocol || !msg.id) return
|
|
if (msg.id === identity().deviceId) return // our own broadcast
|
|
const previous = seen.get(msg.id)
|
|
const next = { id: msg.id, name: String(msg.name || rinfo.address), address: rinfo.address, port: Number(msg.port) || 0, at: Date.now() }
|
|
const changed = !previous || previous.address !== next.address || previous.port !== next.port || previous.name !== next.name
|
|
seen.set(msg.id, next)
|
|
if (changed) emitDevices()
|
|
})
|
|
socket.bind(port, () => {
|
|
try {
|
|
socket.setBroadcast(true)
|
|
} catch {}
|
|
beacon()
|
|
timer = setInterval(beacon, beaconMs)
|
|
})
|
|
}
|
|
|
|
function stop() {
|
|
if (timer) {
|
|
clearInterval(timer)
|
|
timer = null
|
|
}
|
|
if (socket) {
|
|
const closing = socket
|
|
socket = null
|
|
try {
|
|
closing.close()
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
return { start, stop, listDevices }
|
|
}
|
|
|
|
module.exports = { createDiscovery }
|