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:
2026-01-09 09:36:43 +00:00
parent 69a72931dd
commit 181c4f5d5d
21 changed files with 1232 additions and 5707 deletions

369
ts/factories/index.ts Normal file
View 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,
};