'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 }