import * as plugins from './plugins.js'; import { MdnsDiscovery, SERVICE_TYPES } from './discovery/discovery.classes.mdns.js'; import { NetworkScanner } from './discovery/discovery.classes.networkscanner.js'; import { Scanner } from './scanner/scanner.classes.scanner.js'; import { Printer } from './printer/printer.classes.printer.js'; import type { IDeviceManagerOptions, IDiscoveredDevice, IRetryOptions, TDeviceManagerEvents, INetworkScanOptions, INetworkScanResult, } from './interfaces/index.js'; /** * Default device manager options */ const DEFAULT_OPTIONS: Required = { autoDiscovery: true, discoveryTimeout: 10000, enableRetry: true, maxRetries: 5, retryBaseDelay: 1000, }; /** * Main Device Manager class for discovering and managing network devices */ export class DeviceManager extends plugins.events.EventEmitter { private discovery: MdnsDiscovery; private _networkScanner: NetworkScanner | null = null; private scanners: Map = new Map(); private printers: Map = new Map(); private options: Required; private retryOptions: IRetryOptions; constructor(options?: IDeviceManagerOptions) { super(); this.options = { ...DEFAULT_OPTIONS, ...options }; this.retryOptions = { maxRetries: this.options.maxRetries, baseDelay: this.options.retryBaseDelay, maxDelay: 16000, multiplier: 2, jitter: true, }; this.discovery = new MdnsDiscovery({ timeout: this.options.discoveryTimeout, }); this.setupDiscoveryEvents(); } /** * Setup event forwarding from discovery service */ private setupDiscoveryEvents(): void { this.discovery.on('device:found', (device: IDiscoveredDevice) => { this.handleDeviceFound(device); }); this.discovery.on('device:lost', (device: IDiscoveredDevice) => { this.handleDeviceLost(device); }); this.discovery.on('scanner:found', (device: IDiscoveredDevice) => { // Scanner found event is emitted after device:found handling }); this.discovery.on('printer:found', (device: IDiscoveredDevice) => { // Printer found event is emitted after device:found handling }); this.discovery.on('started', () => { this.emit('discovery:started'); }); this.discovery.on('stopped', () => { this.emit('discovery:stopped'); }); } /** * Handle newly discovered device */ private handleDeviceFound(device: IDiscoveredDevice): void { if (device.type === 'scanner') { // Create Scanner instance const scanner = Scanner.fromDiscovery( { id: device.id, name: device.name, address: device.address, port: device.port, protocol: device.protocol as 'sane' | 'escl', txtRecords: device.txtRecords, }, this.options.enableRetry ? this.retryOptions : undefined ); this.scanners.set(device.id, scanner); this.emit('scanner:found', scanner.getScannerInfo()); } else if (device.type === 'printer') { // Create Printer instance const printer = Printer.fromDiscovery( { id: device.id, name: device.name, address: device.address, port: device.port, txtRecords: device.txtRecords, }, this.options.enableRetry ? this.retryOptions : undefined ); this.printers.set(device.id, printer); this.emit('printer:found', printer.getPrinterInfo()); } } /** * Handle lost device */ private handleDeviceLost(device: IDiscoveredDevice): void { if (device.type === 'scanner') { const scanner = this.scanners.get(device.id); if (scanner) { // Disconnect if connected if (scanner.isConnected) { scanner.disconnect().catch(() => {}); } this.scanners.delete(device.id); this.emit('scanner:lost', device.id); } } else if (device.type === 'printer') { const printer = this.printers.get(device.id); if (printer) { // Disconnect if connected if (printer.isConnected) { printer.disconnect().catch(() => {}); } this.printers.delete(device.id); this.emit('printer:lost', device.id); } } } /** * Start device discovery */ public async startDiscovery(): Promise { await this.discovery.start(); } /** * Stop device discovery */ public async stopDiscovery(): Promise { await this.discovery.stop(); } /** * Check if discovery is running */ public get isDiscovering(): boolean { return this.discovery.running; } /** * Get all discovered scanners */ public getScanners(): Scanner[] { return Array.from(this.scanners.values()); } /** * Get all discovered printers */ public getPrinters(): Printer[] { return Array.from(this.printers.values()); } /** * Get scanner by ID */ public getScanner(id: string): Scanner | undefined { return this.scanners.get(id); } /** * Get printer by ID */ public getPrinter(id: string): Printer | undefined { return this.printers.get(id); } /** * Get all devices (scanners and printers) */ public getDevices(): (Scanner | Printer)[] { return [...this.getScanners(), ...this.getPrinters()]; } /** * Get device by ID (scanner or printer) */ public getDevice(id: string): Scanner | Printer | undefined { return this.scanners.get(id) ?? this.printers.get(id); } /** * Add a scanner manually (without discovery) */ public async addScanner( address: string, port: number, protocol: 'escl' | 'sane' = 'escl', name?: string ): Promise { const id = `manual:${protocol}:${address}:${port}`; // Check if already exists if (this.scanners.has(id)) { return this.scanners.get(id)!; } const scanner = Scanner.fromDiscovery( { id, name: name ?? `Scanner at ${address}`, address, port, protocol, txtRecords: {}, }, this.options.enableRetry ? this.retryOptions : undefined ); // Try to connect to validate await scanner.connect(); this.scanners.set(id, scanner); this.emit('scanner:found', scanner.getScannerInfo()); return scanner; } /** * Add a printer manually (without discovery) */ public async addPrinter( address: string, port: number = 631, name?: string, ippPath?: string ): Promise { const id = `manual:ipp:${address}:${port}`; // Check if already exists if (this.printers.has(id)) { return this.printers.get(id)!; } const printer = Printer.fromDiscovery( { id, name: name ?? `Printer at ${address}`, address, port, txtRecords: ippPath ? { rp: ippPath } : {}, }, this.options.enableRetry ? this.retryOptions : undefined ); // Try to connect to validate await printer.connect(); this.printers.set(id, printer); this.emit('printer:found', printer.getPrinterInfo()); return printer; } /** * Get the NetworkScanner instance for advanced control */ public get networkScanner(): NetworkScanner { if (!this._networkScanner) { this._networkScanner = new NetworkScanner(); this.setupNetworkScannerEvents(); } return this._networkScanner; } /** * Setup event forwarding from network scanner */ private setupNetworkScannerEvents(): void { if (!this._networkScanner) return; this._networkScanner.on('device:found', (result: INetworkScanResult) => { this.emit('network:device:found', result); }); this._networkScanner.on('progress', (progress) => { this.emit('network:progress', progress); }); this._networkScanner.on('complete', (results) => { this.emit('network:complete', results); }); this._networkScanner.on('error', (error) => { this.emit('error', error); }); } /** * Scan a network range for devices (IP-based, not mDNS) * Found devices are automatically added to the device manager */ public async scanNetwork( options: INetworkScanOptions ): Promise<{ scanners: Scanner[]; printers: Printer[] }> { const results = await this.networkScanner.scan(options); const foundScanners: Scanner[] = []; const foundPrinters: Printer[] = []; for (const result of results) { for (const device of result.devices) { try { if (device.type === 'scanner') { if (device.protocol === 'escl') { const scanner = await this.addScanner( result.address, device.port, 'escl', device.name ); foundScanners.push(scanner); } else if (device.protocol === 'sane') { const scanner = await this.addScanner( result.address, device.port, 'sane', device.name ); foundScanners.push(scanner); } } else if (device.type === 'printer') { if (device.protocol === 'ipp') { const printer = await this.addPrinter( result.address, device.port, device.name ); foundPrinters.push(printer); } // JetDirect printers don't have a protocol handler yet } } catch (error) { // Device could not be added (connection failed, etc.) this.emit('error', error instanceof Error ? error : new Error(String(error))); } } } return { scanners: foundScanners, printers: foundPrinters }; } /** * Cancel an ongoing network scan */ public async cancelNetworkScan(): Promise { if (this._networkScanner) { await this._networkScanner.cancel(); } } /** * Check if a network scan is in progress */ public get isNetworkScanning(): boolean { return this._networkScanner?.isScanning ?? false; } /** * Remove a device */ public async removeDevice(id: string): Promise { const scanner = this.scanners.get(id); if (scanner) { if (scanner.isConnected) { await scanner.disconnect(); } this.scanners.delete(id); this.emit('scanner:lost', id); return true; } const printer = this.printers.get(id); if (printer) { if (printer.isConnected) { await printer.disconnect(); } this.printers.delete(id); this.emit('printer:lost', id); return true; } return false; } /** * Disconnect all devices */ public async disconnectAll(): Promise { const disconnectPromises: Promise[] = []; for (const scanner of this.scanners.values()) { if (scanner.isConnected) { disconnectPromises.push(scanner.disconnect().catch(() => {})); } } for (const printer of this.printers.values()) { if (printer.isConnected) { disconnectPromises.push(printer.disconnect().catch(() => {})); } } await Promise.all(disconnectPromises); } /** * Stop discovery and disconnect all devices */ public async shutdown(): Promise { await this.stopDiscovery(); await this.disconnectAll(); this.scanners.clear(); this.printers.clear(); } /** * Refresh status of all devices */ public async refreshAllStatus(): Promise { const refreshPromises: Promise[] = []; for (const scanner of this.scanners.values()) { if (scanner.isConnected) { refreshPromises.push( scanner.refreshStatus().catch((error) => { this.emit('error', error); }) ); } } for (const printer of this.printers.values()) { if (printer.isConnected) { refreshPromises.push( printer.refreshStatus().catch((error) => { this.emit('error', error); }) ); } } await Promise.all(refreshPromises); } } export { MdnsDiscovery, NetworkScanner, Scanner, Printer, SERVICE_TYPES };