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 = getNativeFastPrinterState() || {} const patch: NativePrinterResult = { ...prev, ...res, lastAction: 'getDebugInfo', available: isNativeFastPrinterAvailable(), } const resStage = String(res.stage ?? '').trim() if (resStage) patch.stage = resStage if (res.writeMs != null && Number.isFinite(Number(res.writeMs))) { patch.writeMs = Number(res.writeMs) } if (res.commandBytes != null && Number(res.commandBytes) > 0) { patch.commandBytes = Number(res.commandBytes) } if (res.lastError != null && String(res.lastError).trim()) { patch.lastError = String(res.lastError) } updateNativeState(patch) resolve({ ...patch }) }) } 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' } function sleepMs (ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } /** * printCommandBytes 原生仅首帧回调 queued 即 success,真实写出在后台线程。 * 轮询 debugInfo 直到 printCommandBytes:ok / writeMs 增长 / :error。 */ export async function waitForNativeCommandBytesWriteComplete ( timeoutMs = 300000, onPoll?: (percentInWait: number) => void, ): Promise<{ writeMs: number; commandBytes: number; stage: string }> { const started = Date.now() const baselineWriteMs = Number(getNativeFastPrinterState()?.writeMs || 0) while (Date.now() - started < timeoutMs) { try { await getNativeFastPrinterDebugInfo() } catch (_) { /* 单次 getDebugInfo 失败不中断,继续读缓存 state */ } const info = getNativeFastPrinterState() || {} const stage = String(info.stage || '').toLowerCase() const writeMs = Number(info.writeMs || 0) const commandBytes = Number(info.commandBytes || 0) if (stage.includes('printcommandbytes:error')) { throw new Error(String(info.lastError || 'NATIVE_PRINT_COMMAND_BYTES_ERROR')) } if (stage.includes('printcommandbytes:ok')) { return { writeMs, commandBytes, stage: String(info.stage || '') } } /** 部分 AAR stage 字段缺失,但 writeMs 相对 queued 前已增长 → 纸已出 */ if (writeMs > baselineWriteMs && writeMs > 0) { return { writeMs, commandBytes, stage: stage || 'printCommandBytes:ok' } } if (onPoll) { const elapsed = Date.now() - started const cap = Math.min(timeoutMs, 90000) onPoll(Math.min(99, Math.round((elapsed / cap) * 100))) } await sleepMs(150) } throw new Error('NATIVE_PRINT_COMMAND_BYTES_WRITE_TIMEOUT') } 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'))) } }) }