Files
devicemanager/ts/printer/printer.classes.printer.ts

256 lines
6.4 KiB
TypeScript
Raw Permalink Normal View History

2026-01-09 07:14:39 +00:00
import { Device } from '../abstract/device.abstract.js';
import { IppProtocol } from './printer.classes.ippprotocol.js';
import type {
IPrinterInfo,
IPrinterCapabilities,
IPrintOptions,
IPrintJob,
IRetryOptions,
} from '../interfaces/index.js';
/**
* Printer class for IPP network printers
*/
export class Printer extends Device {
public readonly uri: string;
public supportsColor: boolean = false;
public supportsDuplex: boolean = false;
public supportedMediaTypes: string[] = [];
public supportedMediaSizes: string[] = [];
public maxCopies: number = 99;
private ippClient: IppProtocol | null = null;
private ippPath: string;
constructor(
info: IPrinterInfo,
options?: {
ippPath?: string;
retryOptions?: IRetryOptions;
}
) {
super(info, options?.retryOptions);
this.uri = info.uri;
this.supportsColor = info.supportsColor;
this.supportsDuplex = info.supportsDuplex;
this.supportedMediaTypes = info.supportedMediaTypes;
this.supportedMediaSizes = info.supportedMediaSizes;
this.maxCopies = info.maxCopies;
this.ippPath = options?.ippPath ?? '/ipp/print';
}
/**
* Create a Printer from discovery info
*/
public static fromDiscovery(
discoveredDevice: {
id: string;
name: string;
address: string;
port: number;
txtRecords: Record<string, string>;
},
retryOptions?: IRetryOptions
): Printer {
// Parse capabilities from TXT records
const txtRecords = discoveredDevice.txtRecords;
// Get IPP path from TXT records
const rp = txtRecords['rp'] || 'ipp/print';
const ippPath = rp.startsWith('/') ? rp : `/${rp}`;
// Parse color support
const colorSupported =
txtRecords['Color'] === 'T' ||
txtRecords['color'] === 'true' ||
txtRecords['URF']?.includes('W8') ||
false;
// Parse duplex support
const duplexSupported =
txtRecords['Duplex'] === 'T' ||
txtRecords['duplex'] === 'true' ||
txtRecords['URF']?.includes('DM') ||
false;
// Build printer URI
const isSecure = txtRecords['TLS'] === '1' || discoveredDevice.port === 443;
const protocol = isSecure ? 'ipps' : 'ipp';
const uri = `${protocol}://${discoveredDevice.address}:${discoveredDevice.port}${ippPath}`;
const info: IPrinterInfo = {
id: discoveredDevice.id,
name: discoveredDevice.name,
type: 'printer',
address: discoveredDevice.address,
port: discoveredDevice.port,
status: 'online',
uri: uri,
supportsColor: colorSupported,
supportsDuplex: duplexSupported,
supportedMediaTypes: [],
supportedMediaSizes: [],
maxCopies: 99,
manufacturer: txtRecords['usb_MFG'] || txtRecords['mfg'],
model: txtRecords['usb_MDL'] || txtRecords['mdl'] || txtRecords['ty'],
};
return new Printer(info, { ippPath, retryOptions });
}
/**
* Get printer info
*/
public getPrinterInfo(): IPrinterInfo {
return {
...this.getInfo(),
type: 'printer',
uri: this.uri,
supportsColor: this.supportsColor,
supportsDuplex: this.supportsDuplex,
supportedMediaTypes: this.supportedMediaTypes,
supportedMediaSizes: this.supportedMediaSizes,
maxCopies: this.maxCopies,
};
}
/**
* Get printer capabilities
*/
public async getCapabilities(): Promise<IPrinterCapabilities> {
if (!this.isConnected) {
await this.connect();
}
if (!this.ippClient) {
throw new Error('IPP client not initialized');
}
const caps = await this.withRetry(() => this.ippClient!.getAttributes());
// Update local properties
this.supportsColor = caps.colorSupported;
this.supportsDuplex = caps.duplexSupported;
this.supportedMediaSizes = caps.mediaSizes;
this.supportedMediaTypes = caps.mediaTypes;
this.maxCopies = caps.maxCopies;
return caps;
}
/**
* Print a document
*/
public async print(data: Buffer, options?: IPrintOptions): Promise<IPrintJob> {
if (!this.isConnected) {
await this.connect();
}
if (!this.ippClient) {
throw new Error('IPP client not initialized');
}
this.setStatus('busy');
this.emit('print:started', options);
try {
const job = await this.withRetry(() => this.ippClient!.print(data, options));
this.setStatus('online');
this.emit('print:submitted', job);
return job;
} catch (error) {
this.setStatus('online');
this.emit('print:error', error);
throw error;
}
}
/**
* Get all print jobs
*/
public async getJobs(): Promise<IPrintJob[]> {
if (!this.isConnected) {
await this.connect();
}
if (!this.ippClient) {
throw new Error('IPP client not initialized');
}
return this.withRetry(() => this.ippClient!.getJobs());
}
/**
* Get specific job info
*/
public async getJobInfo(jobId: number): Promise<IPrintJob> {
if (!this.isConnected) {
await this.connect();
}
if (!this.ippClient) {
throw new Error('IPP client not initialized');
}
return this.withRetry(() => this.ippClient!.getJobInfo(jobId));
}
/**
* Cancel a print job
*/
public async cancelJob(jobId: number): Promise<void> {
if (!this.isConnected) {
await this.connect();
}
if (!this.ippClient) {
throw new Error('IPP client not initialized');
}
await this.withRetry(() => this.ippClient!.cancelJob(jobId));
this.emit('print:canceled', jobId);
}
/**
* Connect to the printer
*/
protected async doConnect(): Promise<void> {
this.ippClient = new IppProtocol(this.address, this.port, this.ippPath);
// Test connection by checking availability
const available = await this.ippClient.checkAvailability();
if (!available) {
throw new Error('Printer not available');
}
// Fetch capabilities to populate local properties
await this.getCapabilities();
}
/**
* Disconnect from the printer
*/
protected async doDisconnect(): Promise<void> {
this.ippClient = null;
}
/**
* Refresh printer status
*/
public async refreshStatus(): Promise<void> {
try {
if (this.ippClient) {
const available = await this.ippClient.checkAvailability();
this.setStatus(available ? 'online' : 'offline');
} else {
this.setStatus('offline');
}
} catch (error) {
this.setStatus('error');
throw error;
}
}
}
export { IppProtocol };