import { logBuiltinTscCapability } from './builtinTscCapabilityLog' import type { LabelPrintJobPayload } from '../labelPreview/buildLabelPrintPayload' import type { LabelTemplateData, SystemLabelTemplate } from './types/printer' type NativePrinterResult = { code?: number msg?: string errMsg?: string connected?: boolean deviceId?: string deviceName?: string success?: boolean backend?: string pluginVersion?: string stage?: string lastError?: string buildMs?: number writeMs?: number commandBytes?: number lastPrintAt?: number nativeTextCount?: number rasterTextCount?: number qrCodeCount?: number barcodeCount?: number imagePatchCount?: number lineCount?: number elementCount?: number available?: boolean lastAction?: string } const nativeFastPrinterState: NativePrinterResult = { available: false, lastAction: 'idle', } function getUniApi (): any { return uni as any } function parsePluginResult (payload: any): NativePrinterResult { if (!payload) return {} if (typeof payload === 'string') { try { return JSON.parse(payload) } catch (_) { return { msg: payload } } } return payload as NativePrinterResult } function updateNativeState (patch: NativePrinterResult) { Object.assign(nativeFastPrinterState, patch, { available: isNativeFastPrinterAvailable(), }) } function getNativePlugin (): any | null { // #ifdef APP-PLUS try { const api = getUniApi() if (typeof api.requireNativePlugin !== 'function') return null const plugin = api.requireNativePlugin('native-fast-printer') return plugin || null } catch (_) { return null } // #endif // #ifndef APP-PLUS return null // #endif } function ensureNativePlugin (): any { const plugin = getNativePlugin() if (!plugin) { updateNativeState({ available: false, lastAction: 'plugin:missing', lastError: 'NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND', }) throw new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND') } return plugin } export function isNativeFastPrinterAvailable (): boolean { const plugin = getNativePlugin() return !!plugin && typeof plugin.connect === 'function' && typeof plugin.printTemplate === 'function' } export function isNativeUposPrintSupported (): boolean { const plugin = getNativePlugin() return !!plugin && typeof (plugin as any).printUposCommandBytes === 'function' } export function getNativeFastPrinterState (): NativePrinterResult | null { return { ...nativeFastPrinterState, available: isNativeFastPrinterAvailable(), } } function buildTimeoutError (action: string, timeoutMs: number): Error { const snapshot = getNativeFastPrinterState() const detail = [ `action=${action}`, `timeout=${Math.round(timeoutMs / 1000)}s`, snapshot?.backend ? `backend=${snapshot.backend}` : '', snapshot?.stage ? `stage=${snapshot.stage}` : '', snapshot?.commandBytes ? `commandBytes=${snapshot.commandBytes}` : '', snapshot?.lastError ? `lastError=${snapshot.lastError}` : '', ].filter(Boolean).join('\n') return new Error(`Native printer timeout.\n${detail}`.trim()) } function wrapCallback ( action: string, timeoutMs: number, executor: (resolve: (value: NativePrinterResult) => void, reject: (reason?: any) => void) => void ) { return new Promise((resolve, reject) => { let settled = false const timer = setTimeout(() => { if (settled) return settled = true updateNativeState({ lastAction: `${action}:timeout`, }) reject(buildTimeoutError(action, timeoutMs)) }, timeoutMs) const done = (handler: () => void) => { if (settled) return settled = true clearTimeout(timer) handler() } executor( (value) => done(() => resolve(value)), (reason) => done(() => reject(reason)), ) }) } export function getNativeFastPrinterDebugInfo () { return wrapCallback('getDebugInfo', 5000, (resolve, reject) => { try { const nativePlugin = ensureNativePlugin() if (typeof nativePlugin.getDebugInfo !== 'function') { const snapshot = getNativeFastPrinterState() resolve(snapshot || {}) return } nativePlugin.getDebugInfo((payload: any) => { const res = parsePluginResult(payload) const prev = { ...nativeFastPrinterState } const patch: NativePrinterResult = { ...res, lastAction: 'getDebugInfo', } /** getDebugInfo 常只带插件元数据;勿把上次 print 的 commandBytes/writeMs/stage 冲成 0 或空 */ const resBytes = Number(res.commandBytes ?? 0) const resWrite = Number(res.writeMs ?? 0) const prevBytes = Number(prev.commandBytes ?? 0) if ((resBytes <= 0 && resWrite <= 0) && prevBytes > 0) { patch.commandBytes = prev.commandBytes patch.writeMs = prev.writeMs if (res.stage == null || String(res.stage).trim() === '') { patch.stage = prev.stage } } updateNativeState(patch) resolve(res) }) } catch (error: any) { reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_FAST_PRINTER_DEBUG_FAILED'))) } }) } export function connectNativeFastPrinter (options: { deviceId: string deviceName?: string }) { return wrapCallback('connect', 12000, (resolve, reject) => { try { const nativePlugin = ensureNativePlugin() if (typeof nativePlugin.connect !== 'function') { reject(new Error('NATIVE_FAST_PRINTER_CONNECT_METHOD_NOT_FOUND')) return } nativePlugin.connect({ deviceId: options.deviceId, deviceName: options.deviceName || '', }, (payload: any) => { const res = parsePluginResult(payload) updateNativeState({ ...res, lastAction: 'connect', }) if (res.code === 1 || res.success === true) { resolve(res) return } reject(new Error(res.msg || res.errMsg || 'NATIVE_FAST_PRINTER_CONNECT_FAILED')) }) } catch (error: any) { reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_FAST_PRINTER_CONNECT_FAILED'))) } }) } export function disconnectNativeFastPrinter () { return wrapCallback('disconnect', 8000, (resolve, reject) => { try { const nativePlugin = ensureNativePlugin() if (typeof nativePlugin.disconnect !== 'function') { reject(new Error('NATIVE_FAST_PRINTER_DISCONNECT_METHOD_NOT_FOUND')) return } nativePlugin.disconnect((payload: any) => { const res = parsePluginResult(payload) updateNativeState({ ...res, lastAction: 'disconnect', }) if (res.code === 1 || res.success === true) { resolve(res) return } reject(new Error(res.msg || res.errMsg || 'NATIVE_FAST_PRINTER_DISCONNECT_FAILED')) }) } catch (error: any) { reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_FAST_PRINTER_DISCONNECT_FAILED'))) } }) } /** * 与 setLastLabelPrintJobPayload / getLastLabelPrintJobPayload 同构: * templateJson、dataJson 分别 JSON.stringify(template)、JSON.stringify(printInputJson),与平台导出的 label-template JSON 对齐。 * * 注意:Android 插件在任务入队后即回调成功,真正写机在 PRINT_EXECUTOR 后台执行; * 若 SIZE 超限、构建异常等,JS 仍可能已 resolve,需结合 getNativeFastPrinterDebugInfo 或改原生回调时机排查。 */ export function printNativeFastFromLabelPrintJob (options: { deviceId: string deviceName?: string payload: LabelPrintJobPayload dpi?: number printQty?: number /** 内置 TSC:不经蓝牙,走 UPOS 写出(需基座 AAR ≥ 1.2.7) */ outputTransport?: 'bluetooth' | 'upos' uposPrefer?: 'builtin' | 'serial' uposSerialPath?: string uposBaudrate?: number }) { const qty = Math.max(1, options.printQty ?? options.payload.meta?.printQuantity ?? 1) /** UPOS 大标签 + 一体机可能超过 120s;与 printUposCommandBytes 量级对齐 */ const uposMs = options.outputTransport === 'upos' ? 600000 : 20000 return wrapCallback('printTemplate', uposMs, (resolve, reject) => { try { const nativePlugin = ensureNativePlugin() if (typeof nativePlugin.printTemplate !== 'function') { reject(new Error('NATIVE_FAST_PRINTER_PRINT_METHOD_NOT_FOUND')) return } const params: Record = { deviceId: options.deviceId, deviceName: options.deviceName || '', templateJson: JSON.stringify(options.payload.template), dataJson: JSON.stringify(options.payload.printInputJson ?? {}), dpi: options.dpi || 203, printQty: qty, } if (options.outputTransport === 'upos') { params.outputTransport = 'upos' params.uposPrefer = options.uposPrefer || 'builtin' params.uposSerialPath = options.uposSerialPath || '' params.uposBaudrate = Math.max(1200, options.uposBaudrate ?? 9600) logBuiltinTscCapability('printTemplate_upos_invoke', { deviceId: options.deviceId, printQty: qty, uposPrefer: params.uposPrefer, uposBaudrate: params.uposBaudrate, }) } nativePlugin.printTemplate(params, (raw: any) => { const res = parsePluginResult(raw) updateNativeState({ ...res, lastAction: 'printTemplate', }) if (res.code === 1 || res.success === true) { resolve(res) return } reject(new Error(res.msg || res.errMsg || 'NATIVE_FAST_PRINTER_PRINT_FAILED')) }) } catch (error: any) { reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_FAST_PRINTER_PRINT_FAILED'))) } }) } /** * 将已生成的 TSC 等指令字节(Base64)交给原生佳博通道写出;与 connect 使用同一 GprinterBluetoothTransport。 * 需基座 AAR ≥ 1.2.0(含 printCommandBytes);旧包会走 sendToPrinter 回退到 JS 经典蓝牙。 */ export function printNativeCommandBytes (options: { deviceId: string deviceName?: string base64: string }) { return wrapCallback('printCommandBytes', 600000, (resolve, reject) => { try { const nativePlugin = ensureNativePlugin() if (typeof nativePlugin.printCommandBytes !== 'function') { reject(new Error('NATIVE_PRINT_COMMAND_BYTES_NOT_SUPPORTED')) return } nativePlugin.printCommandBytes({ deviceId: options.deviceId, deviceName: options.deviceName || '', base64: options.base64, }, (raw: any) => { const res = parsePluginResult(raw) updateNativeState({ ...res, lastAction: 'printCommandBytes', }) if (res.code === 1 || res.success === true) { resolve(res) return } reject(new Error(res.msg || res.errMsg || 'NATIVE_PRINT_COMMAND_BYTES_FAILED')) }) } catch (error: any) { reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_PRINT_COMMAND_BYTES_FAILED'))) } }) } export function isNativePrintCommandBytesSupported (): boolean { const plugin = getNativePlugin() return !!plugin && typeof plugin.printCommandBytes === 'function' } export function printNativeUposCommandBytes (options: { base64: string prefer?: 'builtin' | 'serial' serialPath?: string baudrate?: number }) { // 标签整页光栅的 TSC 指令可能很大,20s 过短会导致 Promise 先超时、底层仍可能在写/卡住。 return wrapCallback('printUposCommandBytes', 300000, (resolve, reject) => { try { const nativePlugin = ensureNativePlugin() if (typeof (nativePlugin as any).printUposCommandBytes !== 'function') { reject(new Error('NATIVE_UPOS_PRINT_NOT_SUPPORTED')) return } updateNativeState({ lastAction: 'printUposCommandBytes:start', }) ;(nativePlugin as any).printUposCommandBytes({ base64: options.base64, prefer: options.prefer || 'builtin', serialPath: options.serialPath || '', baudrate: options.baudrate || 9600, }, (raw: any) => { const res = parsePluginResult(raw) updateNativeState({ ...res, lastAction: 'printUposCommandBytes', }) if (res.code === 1 || res.success === true) { resolve(res) return } reject(new Error(res.msg || res.errMsg || 'NATIVE_UPOS_PRINT_FAILED')) }) } catch (error: any) { reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_UPOS_PRINT_FAILED'))) } }) } export function printNativeFastTemplate (options: { deviceId: string deviceName?: string template: SystemLabelTemplate data?: LabelTemplateData dpi?: number printQty?: number outputTransport?: 'bluetooth' | 'upos' uposPrefer?: 'builtin' | 'serial' uposSerialPath?: string uposBaudrate?: number }) { const uposMs = options.outputTransport === 'upos' ? 600000 : 20000 return wrapCallback('printTemplate', uposMs, (resolve, reject) => { try { const nativePlugin = ensureNativePlugin() if (typeof nativePlugin.printTemplate !== 'function') { reject(new Error('NATIVE_FAST_PRINTER_PRINT_METHOD_NOT_FOUND')) return } const params: Record = { deviceId: options.deviceId, deviceName: options.deviceName || '', templateJson: JSON.stringify(options.template), dataJson: JSON.stringify(options.data || {}), dpi: options.dpi || 203, printQty: options.printQty || 1, } if (options.outputTransport === 'upos') { params.outputTransport = 'upos' params.uposPrefer = options.uposPrefer || 'builtin' params.uposSerialPath = options.uposSerialPath || '' params.uposBaudrate = Math.max(1200, options.uposBaudrate ?? 9600) logBuiltinTscCapability('printTemplate_upos_invoke', { deviceId: options.deviceId, printQty: options.printQty || 1, uposPrefer: params.uposPrefer, uposBaudrate: params.uposBaudrate, }) } nativePlugin.printTemplate(params, (raw: any) => { const res = parsePluginResult(raw) updateNativeState({ ...res, lastAction: 'printTemplate', }) if (res.code === 1 || res.success === true) { resolve(res) return } reject(new Error(res.msg || res.errMsg || 'NATIVE_FAST_PRINTER_PRINT_FAILED')) }) } catch (error: any) { reject(error instanceof Error ? error : new Error(String(error || 'NATIVE_FAST_PRINTER_PRINT_FAILED'))) } }) }