import Bonjour from 'bonjour-service'; import { WledClient } from '../wled/client'; export interface DiscoveredDevice { name: string; ipAddress: string; port: number; host: string; type: string; } export class DiscoveryService { private bonjour: Bonjour; constructor() { this.bonjour = new Bonjour(); } /** * Discover WLED devices on the network using mDNS * @param timeout - How long to search for devices (in milliseconds) * @returns Array of discovered devices */ async discoverDevices(timeout: number = 5000): Promise { return new Promise((resolve) => { const discoveredDevices: DiscoveredDevice[] = []; const seenAddresses = new Set(); // Search for WLED devices using mDNS service discovery // WLED devices advertise themselves as _http._tcp const browser = this.bonjour.find({ type: 'http' }); browser.on('up', (service) => { // WLED devices typically have "wled" in their name or txt records const isWled = service.name?.toLowerCase().includes('wled') || service.txt?.ver?.startsWith('0.') || // WLED version format service.txt?.name !== undefined; if (isWled && service.addresses && service.addresses.length > 0) { // Use the first IPv4 address const ipv4Address = service.addresses.find((addr: string) => addr.includes('.') && !addr.startsWith('169.254') ); if (ipv4Address && !seenAddresses.has(ipv4Address)) { seenAddresses.add(ipv4Address); discoveredDevices.push({ name: service.name || service.host || 'Unknown WLED Device', ipAddress: ipv4Address, port: service.port || 80, host: service.host || ipv4Address, type: service.type || 'http', }); } } }); // Stop searching after timeout setTimeout(() => { browser.stop(); resolve(discoveredDevices); }, timeout); }); } /** * Scan a range of IP addresses for WLED devices * This is a fallback method when mDNS doesn't work * @param baseIp - Base IP address (e.g., "192.168.1") * @param startRange - Start of range (e.g., 1) * @param endRange - End of range (e.g., 254) * @returns Array of discovered devices */ async scanIpRange( baseIp: string, startRange: number = 1, endRange: number = 254 ): Promise { const discoveredDevices: DiscoveredDevice[] = []; const promises: Promise[] = []; for (let i = startRange; i <= endRange; i++) { const ipAddress = `${baseIp}.${i}`; promises.push( (async () => { try { const client = new WledClient(ipAddress, 80); const info = await Promise.race([ client.getInfo(), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 2000) ), ]) as any; if (info && info.name) { discoveredDevices.push({ name: info.name, ipAddress, port: 80, host: ipAddress, type: 'wled', }); } } catch (err) { // Device not found or not responding, ignore } })() ); // Process in batches of 10 to avoid overwhelming the network if (promises.length >= 10) { await Promise.all(promises); promises.length = 0; } } // Wait for remaining promises if (promises.length > 0) { await Promise.all(promises); } return discoveredDevices; } /** * Verify a device at a specific IP address is a WLED device * @param ipAddress - IP address to check * @param port - Port number (default: 80) * @returns Device info if it's a WLED device, null otherwise */ async verifyDevice( ipAddress: string, port: number = 80 ): Promise { try { const client = new WledClient(ipAddress, port); const info = await Promise.race([ client.getInfo(), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000) ), ]) as any; if (info && info.name) { return { name: info.name, ipAddress, port, host: ipAddress, type: 'wled', }; } return null; } catch (err) { return null; } } destroy() { this.bonjour.destroy(); } } export const discoveryService = new DiscoveryService();