initial
This commit is contained in:
477
ts/devicemanager.classes.devicemanager.ts
Normal file
477
ts/devicemanager.classes.devicemanager.ts
Normal file
@@ -0,0 +1,477 @@
|
||||
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<IDeviceManagerOptions> = {
|
||||
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<string, Scanner> = new Map();
|
||||
private printers: Map<string, Printer> = new Map();
|
||||
private options: Required<IDeviceManagerOptions>;
|
||||
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<void> {
|
||||
await this.discovery.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop device discovery
|
||||
*/
|
||||
public async stopDiscovery(): Promise<void> {
|
||||
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<Scanner> {
|
||||
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<Printer> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
const disconnectPromises: Promise<void>[] = [];
|
||||
|
||||
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<void> {
|
||||
await this.stopDiscovery();
|
||||
await this.disconnectAll();
|
||||
this.scanners.clear();
|
||||
this.printers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh status of all devices
|
||||
*/
|
||||
public async refreshAllStatus(): Promise<void> {
|
||||
const refreshPromises: Promise<void>[] = [];
|
||||
|
||||
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 };
|
||||
Reference in New Issue
Block a user