BREAKING CHANGE(core): rework core device architecture: consolidate protocols into a protocols/ module, introduce UniversalDevice + factories, and remove many legacy device-specific classes (breaking API changes)
This commit is contained in:
369
ts/factories/index.ts
Normal file
369
ts/factories/index.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
Reference in New Issue
Block a user