feat(devicemanager): add selector-based device APIs, selectFeature helper, convenience discovery methods, and ESCL scan completion fallback
This commit is contained in:
@@ -38,6 +38,7 @@ import type {
|
||||
INetworkScanOptions,
|
||||
INetworkScanResult,
|
||||
TFeatureType,
|
||||
IDeviceSelector,
|
||||
} from './interfaces/index.js';
|
||||
|
||||
/**
|
||||
@@ -567,14 +568,48 @@ export class DeviceManager extends plugins.events.EventEmitter {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all devices
|
||||
* Get devices matching the selector criteria.
|
||||
* Returns all devices if no selector provided, or devices matching all criteria.
|
||||
* @param selector Optional criteria to filter devices
|
||||
* @returns Array of matching devices (empty if no matches)
|
||||
*/
|
||||
public getDevices(): UniversalDevice[] {
|
||||
return Array.from(this.devicesByIp.values());
|
||||
public getDevices(selector?: IDeviceSelector): UniversalDevice[] {
|
||||
const devices = Array.from(this.devicesByIp.values());
|
||||
|
||||
if (!selector) {
|
||||
return devices;
|
||||
}
|
||||
|
||||
return devices.filter((device) => this.matchesSelector(device, selector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Select exactly ONE device matching the criteria.
|
||||
* Use this when you expect a specific device and want fail-fast behavior.
|
||||
* @param selector Criteria to match the device
|
||||
* @returns The matching device
|
||||
* @throws Error if no device matches the selector
|
||||
*/
|
||||
public selectDevice(selector: IDeviceSelector): UniversalDevice {
|
||||
const matches = this.getDevices(selector);
|
||||
|
||||
if (matches.length === 0) {
|
||||
throw new Error(`No device found matching: ${JSON.stringify(selector)}`);
|
||||
}
|
||||
|
||||
if (matches.length > 1 && !selector.address && !selector.id) {
|
||||
// Multiple matches without unique identifier - log warning
|
||||
console.warn(
|
||||
`Multiple devices (${matches.length}) match selector, using first: ${matches[0].name}`
|
||||
);
|
||||
}
|
||||
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device by ID
|
||||
* @deprecated Use getDevices({ id }) or selectDevice({ id }) instead
|
||||
*/
|
||||
public getDevice(id: string): UniversalDevice | undefined {
|
||||
for (const device of this.devicesByIp.values()) {
|
||||
@@ -587,11 +622,52 @@ export class DeviceManager extends plugins.events.EventEmitter {
|
||||
|
||||
/**
|
||||
* Get device by address
|
||||
* @deprecated Use getDevices({ address }) or selectDevice({ address }) instead
|
||||
*/
|
||||
public getDeviceByAddress(address: string): UniversalDevice | undefined {
|
||||
return this.devicesByIp.get(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a device matches the selector criteria
|
||||
*/
|
||||
private matchesSelector(device: UniversalDevice, selector: IDeviceSelector): boolean {
|
||||
// Identity checks (exact match)
|
||||
if (selector.id && device.id !== selector.id) {
|
||||
return false;
|
||||
}
|
||||
if (selector.address && device.address !== selector.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attribute checks (partial match, case-insensitive)
|
||||
if (selector.name && !device.name.toLowerCase().includes(selector.name.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
if (selector.model && !device.model?.toLowerCase().includes(selector.model.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
selector.manufacturer &&
|
||||
!device.manufacturer?.toLowerCase().includes(selector.manufacturer.toLowerCase())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Capability checks
|
||||
if (selector.hasFeature && !device.hasFeature(selector.hasFeature)) {
|
||||
return false;
|
||||
}
|
||||
if (selector.hasFeatures && !device.hasFeatures(selector.hasFeatures)) {
|
||||
return false;
|
||||
}
|
||||
if (selector.hasAnyFeature && !device.hasAnyFeature(selector.hasAnyFeature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Device Access - By Feature Type
|
||||
// ============================================================================
|
||||
@@ -982,6 +1058,68 @@ export class DeviceManager extends plugins.events.EventEmitter {
|
||||
return foundDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover scanners in a subnet and add them to the manager.
|
||||
* Convenience method that focuses discovery on scanner protocols only (eSCL, SANE).
|
||||
*
|
||||
* @param subnet CIDR notation subnet (e.g., '192.168.1.0/24')
|
||||
* @param options Optional discovery settings
|
||||
* @returns Array of discovered scanner devices
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const scanners = await manager.discoverScanners('192.168.190.0/24');
|
||||
* for (const scanner of scanners) {
|
||||
* console.log(`Found: ${scanner.name} at ${scanner.address}`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public async discoverScanners(
|
||||
subnet: string,
|
||||
options?: { timeout?: number; concurrency?: number }
|
||||
): Promise<UniversalDevice[]> {
|
||||
await this.scanNetwork({
|
||||
ipRange: subnet,
|
||||
probeEscl: true,
|
||||
probeSane: true,
|
||||
probeIpp: false,
|
||||
probeAirplay: false,
|
||||
probeSonos: false,
|
||||
probeChromecast: false,
|
||||
concurrency: options?.concurrency ?? 50,
|
||||
timeout: options?.timeout ?? 3000,
|
||||
});
|
||||
|
||||
return this.getScanners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover printers in a subnet and add them to the manager.
|
||||
* Convenience method that focuses discovery on printer protocols only (IPP).
|
||||
*
|
||||
* @param subnet CIDR notation subnet (e.g., '192.168.1.0/24')
|
||||
* @param options Optional discovery settings
|
||||
* @returns Array of discovered printer devices
|
||||
*/
|
||||
public async discoverPrinters(
|
||||
subnet: string,
|
||||
options?: { timeout?: number; concurrency?: number }
|
||||
): Promise<UniversalDevice[]> {
|
||||
await this.scanNetwork({
|
||||
ipRange: subnet,
|
||||
probeEscl: false,
|
||||
probeSane: false,
|
||||
probeIpp: true,
|
||||
probeAirplay: false,
|
||||
probeSonos: false,
|
||||
probeChromecast: false,
|
||||
concurrency: options?.concurrency ?? 50,
|
||||
timeout: options?.timeout ?? 3000,
|
||||
});
|
||||
|
||||
return this.getPrinters();
|
||||
}
|
||||
|
||||
public async cancelNetworkScan(): Promise<void> {
|
||||
if (this._networkScanner) {
|
||||
await this._networkScanner.cancel();
|
||||
|
||||
Reference in New Issue
Block a user