370 lines
11 KiB
TypeScript
370 lines
11 KiB
TypeScript
|
|
/**
|
||
|
|
* Device Factory Functions
|
||
|
|
* Create UniversalDevice instances with appropriate features
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { UniversalDevice, type IDeviceCreateOptions } from '../device/device.classes.device.js';
|
||
|
|
import { ScanFeature, type IScanFeatureOptions } from '../features/feature.scan.js';
|
||
|
|
import { PrintFeature, type IPrintFeatureOptions } from '../features/feature.print.js';
|
||
|
|
import { PlaybackFeature, type IPlaybackFeatureOptions } from '../features/feature.playback.js';
|
||
|
|
import { VolumeFeature, type IVolumeFeatureOptions } from '../features/feature.volume.js';
|
||
|
|
import { PowerFeature, type IPowerFeatureOptions } from '../features/feature.power.js';
|
||
|
|
import { SnmpFeature, type ISnmpFeatureOptions } from '../features/feature.snmp.js';
|
||
|
|
import type {
|
||
|
|
TScannerProtocol,
|
||
|
|
TScanFormat,
|
||
|
|
TColorMode,
|
||
|
|
TScanSource,
|
||
|
|
IRetryOptions,
|
||
|
|
} from '../interfaces/index.js';
|
||
|
|
import type { TPrintProtocol } from '../interfaces/feature.interfaces.js';
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Scanner Factory
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
export interface IScannerDiscoveryInfo {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
address: string;
|
||
|
|
port: number;
|
||
|
|
protocol: TScannerProtocol | 'ipp';
|
||
|
|
txtRecords: Record<string, string>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a scanner device (UniversalDevice with ScanFeature)
|
||
|
|
*/
|
||
|
|
export function createScanner(
|
||
|
|
info: IScannerDiscoveryInfo,
|
||
|
|
retryOptions?: IRetryOptions
|
||
|
|
): UniversalDevice {
|
||
|
|
const protocol = info.protocol === 'ipp' ? 'escl' : info.protocol;
|
||
|
|
const isSecure = info.txtRecords['TLS'] === '1' || (protocol === 'escl' && info.port === 443);
|
||
|
|
|
||
|
|
// Parse capabilities from TXT records
|
||
|
|
const formats = parseScanFormats(info.txtRecords);
|
||
|
|
const resolutions = parseScanResolutions(info.txtRecords);
|
||
|
|
const colorModes = parseScanColorModes(info.txtRecords);
|
||
|
|
const sources = parseScanSources(info.txtRecords);
|
||
|
|
|
||
|
|
const device = new UniversalDevice(info.address, info.port, {
|
||
|
|
name: info.name,
|
||
|
|
manufacturer: info.txtRecords['usb_MFG'] || info.txtRecords['mfg'],
|
||
|
|
model: info.txtRecords['usb_MDL'] || info.txtRecords['mdl'] || info.txtRecords['ty'],
|
||
|
|
retryOptions,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Override the generated ID with discovery ID
|
||
|
|
(device as { id: string }).id = info.id;
|
||
|
|
|
||
|
|
// Add scan feature
|
||
|
|
const scanFeature = new ScanFeature(device.getDeviceReference(), info.port, {
|
||
|
|
protocol: protocol as 'escl' | 'sane',
|
||
|
|
secure: isSecure,
|
||
|
|
supportedFormats: formats,
|
||
|
|
supportedResolutions: resolutions,
|
||
|
|
supportedColorModes: colorModes,
|
||
|
|
supportedSources: sources,
|
||
|
|
hasAdf: sources.includes('adf') || sources.includes('adf-duplex'),
|
||
|
|
hasDuplex: sources.includes('adf-duplex'),
|
||
|
|
});
|
||
|
|
|
||
|
|
device.addFeature(scanFeature);
|
||
|
|
return device;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Printer Factory
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
export interface IPrinterDiscoveryInfo {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
address: string;
|
||
|
|
port: number;
|
||
|
|
txtRecords: Record<string, string>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a printer device (UniversalDevice with PrintFeature)
|
||
|
|
*/
|
||
|
|
export function createPrinter(
|
||
|
|
info: IPrinterDiscoveryInfo,
|
||
|
|
retryOptions?: IRetryOptions
|
||
|
|
): UniversalDevice {
|
||
|
|
const ippPath = info.txtRecords['rp'] || info.txtRecords['rfo'] || '/ipp/print';
|
||
|
|
const uri = `ipp://${info.address}:${info.port}${ippPath.startsWith('/') ? '' : '/'}${ippPath}`;
|
||
|
|
|
||
|
|
const device = new UniversalDevice(info.address, info.port, {
|
||
|
|
name: info.name,
|
||
|
|
manufacturer: info.txtRecords['usb_MFG'] || info.txtRecords['mfg'],
|
||
|
|
model: info.txtRecords['usb_MDL'] || info.txtRecords['mdl'] || info.txtRecords['ty'],
|
||
|
|
retryOptions,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Override the generated ID with discovery ID
|
||
|
|
(device as { id: string }).id = info.id;
|
||
|
|
|
||
|
|
// Add print feature
|
||
|
|
const printFeature = new PrintFeature(device.getDeviceReference(), info.port, {
|
||
|
|
protocol: 'ipp',
|
||
|
|
uri,
|
||
|
|
supportsColor: info.txtRecords['Color'] === 'T' || info.txtRecords['color'] === 'true',
|
||
|
|
supportsDuplex: info.txtRecords['Duplex'] === 'T' || info.txtRecords['duplex'] === 'true',
|
||
|
|
});
|
||
|
|
|
||
|
|
device.addFeature(printFeature);
|
||
|
|
return device;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// SNMP Device Factory
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
export interface ISnmpDiscoveryInfo {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
address: string;
|
||
|
|
port: number;
|
||
|
|
community?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create an SNMP device (UniversalDevice with SnmpFeature)
|
||
|
|
*/
|
||
|
|
export function createSnmpDevice(
|
||
|
|
info: ISnmpDiscoveryInfo,
|
||
|
|
retryOptions?: IRetryOptions
|
||
|
|
): UniversalDevice {
|
||
|
|
const device = new UniversalDevice(info.address, info.port, {
|
||
|
|
name: info.name,
|
||
|
|
retryOptions,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Override the generated ID with discovery ID
|
||
|
|
(device as { id: string }).id = info.id;
|
||
|
|
|
||
|
|
// Add SNMP feature
|
||
|
|
const snmpFeature = new SnmpFeature(device.getDeviceReference(), info.port, {
|
||
|
|
community: info.community ?? 'public',
|
||
|
|
});
|
||
|
|
|
||
|
|
device.addFeature(snmpFeature);
|
||
|
|
return device;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// UPS Device Factory
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
export interface IUpsDiscoveryInfo {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
address: string;
|
||
|
|
port: number;
|
||
|
|
protocol: 'nut' | 'snmp';
|
||
|
|
upsName?: string;
|
||
|
|
community?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a UPS device (UniversalDevice with PowerFeature)
|
||
|
|
*/
|
||
|
|
export function createUpsDevice(
|
||
|
|
info: IUpsDiscoveryInfo,
|
||
|
|
retryOptions?: IRetryOptions
|
||
|
|
): UniversalDevice {
|
||
|
|
const device = new UniversalDevice(info.address, info.port, {
|
||
|
|
name: info.name,
|
||
|
|
retryOptions,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Override the generated ID with discovery ID
|
||
|
|
(device as { id: string }).id = info.id;
|
||
|
|
|
||
|
|
// Add power feature
|
||
|
|
const powerFeature = new PowerFeature(device.getDeviceReference(), info.port, {
|
||
|
|
protocol: info.protocol,
|
||
|
|
upsName: info.upsName,
|
||
|
|
community: info.community,
|
||
|
|
});
|
||
|
|
|
||
|
|
device.addFeature(powerFeature);
|
||
|
|
return device;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Speaker Factory
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
export interface ISpeakerDiscoveryInfo {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
address: string;
|
||
|
|
port: number;
|
||
|
|
protocol: 'sonos' | 'airplay' | 'chromecast' | 'dlna';
|
||
|
|
roomName?: string;
|
||
|
|
modelName?: string;
|
||
|
|
features?: number; // AirPlay feature flags
|
||
|
|
deviceId?: string;
|
||
|
|
friendlyName?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a speaker device (UniversalDevice with PlaybackFeature and VolumeFeature)
|
||
|
|
*/
|
||
|
|
export function createSpeaker(
|
||
|
|
info: ISpeakerDiscoveryInfo,
|
||
|
|
retryOptions?: IRetryOptions
|
||
|
|
): UniversalDevice {
|
||
|
|
const device = new UniversalDevice(info.address, info.port, {
|
||
|
|
name: info.name,
|
||
|
|
model: info.modelName,
|
||
|
|
retryOptions,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Override the generated ID with discovery ID
|
||
|
|
(device as { id: string }).id = info.id;
|
||
|
|
|
||
|
|
// Add playback feature
|
||
|
|
const playbackFeature = new PlaybackFeature(device.getDeviceReference(), info.port, {
|
||
|
|
protocol: info.protocol,
|
||
|
|
supportsQueue: info.protocol === 'sonos',
|
||
|
|
supportsSeek: info.protocol !== 'airplay',
|
||
|
|
});
|
||
|
|
|
||
|
|
device.addFeature(playbackFeature);
|
||
|
|
|
||
|
|
// Add volume feature
|
||
|
|
const volumeFeature = new VolumeFeature(device.getDeviceReference(), info.port, {
|
||
|
|
volumeProtocol: info.protocol,
|
||
|
|
minVolume: 0,
|
||
|
|
maxVolume: 100,
|
||
|
|
supportsMute: true,
|
||
|
|
});
|
||
|
|
|
||
|
|
device.addFeature(volumeFeature);
|
||
|
|
|
||
|
|
return device;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// DLNA Factory
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
export interface IDlnaRendererDiscoveryInfo {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
address: string;
|
||
|
|
port: number;
|
||
|
|
controlUrl: string;
|
||
|
|
friendlyName: string;
|
||
|
|
modelName?: string;
|
||
|
|
manufacturer?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a DLNA renderer device (UniversalDevice with PlaybackFeature)
|
||
|
|
*/
|
||
|
|
export function createDlnaRenderer(
|
||
|
|
info: IDlnaRendererDiscoveryInfo,
|
||
|
|
retryOptions?: IRetryOptions
|
||
|
|
): UniversalDevice {
|
||
|
|
const device = new UniversalDevice(info.address, info.port, {
|
||
|
|
name: info.friendlyName || info.name,
|
||
|
|
manufacturer: info.manufacturer,
|
||
|
|
model: info.modelName,
|
||
|
|
retryOptions,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Override the generated ID with discovery ID
|
||
|
|
(device as { id: string }).id = info.id;
|
||
|
|
|
||
|
|
// Add playback feature for DLNA
|
||
|
|
const playbackFeature = new PlaybackFeature(device.getDeviceReference(), info.port, {
|
||
|
|
protocol: 'dlna',
|
||
|
|
supportsQueue: false,
|
||
|
|
supportsSeek: true,
|
||
|
|
});
|
||
|
|
|
||
|
|
device.addFeature(playbackFeature);
|
||
|
|
|
||
|
|
// Add volume feature
|
||
|
|
const volumeFeature = new VolumeFeature(device.getDeviceReference(), info.port, {
|
||
|
|
volumeProtocol: 'dlna',
|
||
|
|
minVolume: 0,
|
||
|
|
maxVolume: 100,
|
||
|
|
supportsMute: true,
|
||
|
|
});
|
||
|
|
|
||
|
|
device.addFeature(volumeFeature);
|
||
|
|
|
||
|
|
return device;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Parsing Helpers
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
function parseScanFormats(txtRecords: Record<string, string>): TScanFormat[] {
|
||
|
|
const formats: TScanFormat[] = [];
|
||
|
|
const pdl = txtRecords['pdl'] || txtRecords['DocumentFormat'] || '';
|
||
|
|
|
||
|
|
if (pdl.includes('jpeg') || pdl.includes('jpg')) formats.push('jpeg');
|
||
|
|
if (pdl.includes('png')) formats.push('png');
|
||
|
|
if (pdl.includes('pdf')) formats.push('pdf');
|
||
|
|
if (pdl.includes('tiff')) formats.push('tiff');
|
||
|
|
|
||
|
|
return formats.length > 0 ? formats : ['jpeg', 'png'];
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseScanResolutions(txtRecords: Record<string, string>): number[] {
|
||
|
|
const rs = txtRecords['rs'] || '';
|
||
|
|
const parts = rs.split(',').map((s) => parseInt(s.trim())).filter((n) => !isNaN(n) && n > 0);
|
||
|
|
return parts.length > 0 ? parts : [75, 150, 300, 600];
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseScanColorModes(txtRecords: Record<string, string>): TColorMode[] {
|
||
|
|
const cs = txtRecords['cs'] || txtRecords['ColorSpace'] || '';
|
||
|
|
const modes: TColorMode[] = [];
|
||
|
|
|
||
|
|
if (cs.includes('color') || cs.includes('RGB')) modes.push('color');
|
||
|
|
if (cs.includes('gray') || cs.includes('grayscale')) modes.push('grayscale');
|
||
|
|
if (cs.includes('binary') || cs.includes('bw')) modes.push('blackwhite');
|
||
|
|
|
||
|
|
return modes.length > 0 ? modes : ['color', 'grayscale'];
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseScanSources(txtRecords: Record<string, string>): TScanSource[] {
|
||
|
|
const is = txtRecords['is'] || txtRecords['InputSource'] || '';
|
||
|
|
const sources: TScanSource[] = [];
|
||
|
|
|
||
|
|
if (is.includes('platen') || is.includes('flatbed') || is === '') {
|
||
|
|
sources.push('flatbed');
|
||
|
|
}
|
||
|
|
if (is.includes('adf') || is.includes('feeder')) {
|
||
|
|
sources.push('adf');
|
||
|
|
}
|
||
|
|
if (is.includes('duplex')) {
|
||
|
|
sources.push('adf-duplex');
|
||
|
|
}
|
||
|
|
|
||
|
|
return sources.length > 0 ? sources : ['flatbed'];
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Exports
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
export {
|
||
|
|
// Re-export device and feature types for convenience
|
||
|
|
UniversalDevice,
|
||
|
|
ScanFeature,
|
||
|
|
PrintFeature,
|
||
|
|
PlaybackFeature,
|
||
|
|
VolumeFeature,
|
||
|
|
PowerFeature,
|
||
|
|
SnmpFeature,
|
||
|
|
};
|