code refactor & sync bugfixes
This commit is contained in:
parent
2bcb47076a
commit
9edcc9841b
20 changed files with 820 additions and 812 deletions
135
src/lansync-discovery.js
Normal file
135
src/lansync-discovery.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
'use strict'
|
||||
|
||||
// UDP discovery for the LAN sync: every running PLWL beacons on the local
|
||||
// network and listens for the same beacon, so the devices list fills itself.
|
||||
// The beacon doubles as a liveness signal - a device drops off the list a few
|
||||
// seconds after its PLWL is closed.
|
||||
|
||||
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 }
|
||||
Loading…
Add table
Add a link
Reference in a new issue