/** * 打印机连接与下发:蓝牙(BLE) / 一体机(TCP localhost) */ import type { ActiveBtDeviceType, PrinterType } from './types/printer' import classicBluetooth from './bluetoothTool.js' import { getDeviceFingerprint, isAoaAioDevice } from '../deviceInfo' import { blePairRequiresWriteNoResponse, isNordicUartStyleBleService, normalizeBleUuid, } from './bleWriteModeRules' import { getPrinterDriverByKey } from './manager/driverRegistry' import { hasUserAcknowledgedBuiltinTsc } from './builtinTscCapabilityLog' import { connectNativeFastPrinter, getNativeFastPrinterDebugInfo, isNativeFastPrinterAvailable, isNativeUposPrintSupported, isNativePrintCommandBytesSupported, printNativeCommandBytes, printNativeUposCommandBytes, waitForNativeCommandBytesWriteComplete, } from './nativeFastPrinter' import { printRunDiag } from './printRunDiagnostics' const STORAGE_PRINTER_TYPE = 'printerType' const STORAGE_BT_DEVICE_ID = 'btDeviceId' const STORAGE_BT_DEVICE_NAME = 'btDeviceName' const STORAGE_BT_SERVICE_ID = 'btServiceId' const STORAGE_BT_CHARACTERISTIC_ID = 'btCharacteristicId' const STORAGE_BT_DEVICE_TYPE = 'btDeviceType' // 'ble' | 'classic' const STORAGE_BT_TRANSPORT_MODE = 'btTransportMode' // 'native-plugin' | 'generic' const STORAGE_BLE_MTU = 'bleMTU' /** '1' = 仅支持 writeNoResponse 的特征,需在 writeBLECharacteristicValue 里指定 writeType */ const STORAGE_BLE_WRITE_NO_RESPONSE = 'bleWriteNoResponse' const STORAGE_BUILTIN_PORT = 'builtinPort' const STORAGE_PRINTER_DRIVER_KEY = 'printerDriverKey' const STORAGE_UPOS_PREFER = 'uposPrefer' // 'builtin' | 'serial' const STORAGE_UPOS_SERIAL_PATH = 'uposSerialPath' const STORAGE_UPOS_BAUDRATE = 'uposBaudrate' const STORAGE_UPOS_FORCE = 'uposForce' // '1' | '0' const STORAGE_PRINTER_DEBUG_LOG = 'printerDebugLog' // '1' | '0' const BUILTIN_PROBE_PORTS = [9100, 4000, 9000, 6000] const BUILTIN_PRINTER_DEVICE_KEYWORDS: string[] = [ // 在这里补充需要走 Built-in 的设备型号关键字(小写匹配) 'rk3568', 'aoa_rk3568', 'aoa', ] export type BtDeviceType = ActiveBtDeviceType export const PrinterStorageKeys = { type: STORAGE_PRINTER_TYPE, btDeviceId: STORAGE_BT_DEVICE_ID, btDeviceName: STORAGE_BT_DEVICE_NAME, btServiceId: STORAGE_BT_SERVICE_ID, btCharacteristicId: STORAGE_BT_CHARACTERISTIC_ID, btDeviceType: STORAGE_BT_DEVICE_TYPE, btTransportMode: STORAGE_BT_TRANSPORT_MODE, bleMTU: STORAGE_BLE_MTU, bleWriteNoResponse: STORAGE_BLE_WRITE_NO_RESPONSE, driverKey: STORAGE_PRINTER_DRIVER_KEY, uposPrefer: STORAGE_UPOS_PREFER, uposSerialPath: STORAGE_UPOS_SERIAL_PATH, uposBaudrate: STORAGE_UPOS_BAUDRATE, uposForce: STORAGE_UPOS_FORCE, debugLog: STORAGE_PRINTER_DEBUG_LOG, } as const export function setPrinterType (type: PrinterType) { uni.setStorageSync(STORAGE_PRINTER_TYPE, type) } export function setBluetoothConnection (info: { deviceId: string deviceName: string serviceId?: string characteristicId?: string deviceType?: BtDeviceType transportMode?: 'native-plugin' | 'generic' mtu?: number driverKey?: string /** 当前特征是否必须走 writeNoResponse(仅 write 为 false 时) */ bleWriteUsesNoResponse?: boolean }) { uni.setStorageSync(STORAGE_PRINTER_TYPE, 'bluetooth') uni.setStorageSync(STORAGE_BT_DEVICE_ID, info.deviceId) uni.setStorageSync(STORAGE_BT_DEVICE_NAME, info.deviceName) uni.setStorageSync(STORAGE_BT_SERVICE_ID, info.serviceId || '') uni.setStorageSync(STORAGE_BT_CHARACTERISTIC_ID, info.characteristicId || '') uni.setStorageSync(STORAGE_BT_DEVICE_TYPE, info.deviceType || 'ble') uni.setStorageSync( STORAGE_BT_TRANSPORT_MODE, info.transportMode || (info.deviceType === 'classic' ? 'generic' : 'generic') ) uni.setStorageSync(STORAGE_BLE_MTU, info.mtu != null ? info.mtu : BLE_MTU_DEFAULT) uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, info.driverKey || '') if (info.deviceType === 'ble' || !info.deviceType) { uni.setStorageSync(STORAGE_BLE_WRITE_NO_RESPONSE, info.bleWriteUsesNoResponse ? '1' : '0') } else { uni.setStorageSync(STORAGE_BLE_WRITE_NO_RESPONSE, '0') } } export function setBuiltinPrinter (driverKey = 'generic-tsc') { uni.setStorageSync(STORAGE_PRINTER_TYPE, 'builtin') uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, driverKey) } export function clearPrinter () { uni.removeStorageSync(STORAGE_PRINTER_TYPE) uni.removeStorageSync(STORAGE_BT_DEVICE_ID) uni.removeStorageSync(STORAGE_BT_DEVICE_NAME) uni.removeStorageSync(STORAGE_BT_SERVICE_ID) uni.removeStorageSync(STORAGE_BT_CHARACTERISTIC_ID) uni.removeStorageSync(STORAGE_BT_DEVICE_TYPE) uni.removeStorageSync(STORAGE_BT_TRANSPORT_MODE) uni.removeStorageSync(STORAGE_BLE_MTU) uni.removeStorageSync(STORAGE_BLE_WRITE_NO_RESPONSE) uni.removeStorageSync(STORAGE_BUILTIN_PORT) uni.removeStorageSync(STORAGE_PRINTER_DRIVER_KEY) uni.removeStorageSync(STORAGE_UPOS_PREFER) uni.removeStorageSync(STORAGE_UPOS_SERIAL_PATH) uni.removeStorageSync(STORAGE_UPOS_BAUDRATE) uni.removeStorageSync(STORAGE_UPOS_FORCE) uni.removeStorageSync(STORAGE_PRINTER_DEBUG_LOG) } export function setUposOptions (options: { prefer?: 'builtin' | 'serial' serialPath?: string baudrate?: number /** 强制尝试 UPOS(不依赖机型关键字);用于新机型接入/排查 */ force?: boolean }) { if (options.prefer) uni.setStorageSync(STORAGE_UPOS_PREFER, options.prefer) if (options.serialPath != null) uni.setStorageSync(STORAGE_UPOS_SERIAL_PATH, String(options.serialPath || '')) if (options.baudrate != null) uni.setStorageSync(STORAGE_UPOS_BAUDRATE, String(Math.max(1200, Math.floor(options.baudrate || 9600)))) if (typeof options.force === 'boolean') uni.setStorageSync(STORAGE_UPOS_FORCE, options.force ? '1' : '0') } /** 供 printerManager / native 模板 UPOS 下发读取(与内置打印页 UPOS 选项同源) */ export function getStoredUposPrintOptions (): { prefer: 'builtin' | 'serial' serialPath?: string baudrate: number force: boolean } { const preferRaw = String(uni.getStorageSync(STORAGE_UPOS_PREFER) || '').toLowerCase() const prefer = (preferRaw === 'serial' ? 'serial' : 'builtin') as 'builtin' | 'serial' const serialPath = String(uni.getStorageSync(STORAGE_UPOS_SERIAL_PATH) || '').trim() const baudrate = Math.max(1200, parseInt(String(uni.getStorageSync(STORAGE_UPOS_BAUDRATE) || '9600')) || 9600) const force = uni.getStorageSync(STORAGE_UPOS_FORCE) === '1' return { prefer, // 避免 “prefer=builtin 但仍携带串口路径” 造成 preflight/排查误导 serialPath: (prefer === 'serial' && serialPath) ? serialPath : undefined, baudrate, force, } } function getUposOptionsFromStorage () { return getStoredUposPrintOptions() } export function setPrinterDebugLogEnabled (enabled: boolean) { uni.setStorageSync(STORAGE_PRINTER_DEBUG_LOG, enabled ? '1' : '0') } export function isPrinterDebugLogEnabled (): boolean { return uni.getStorageSync(STORAGE_PRINTER_DEBUG_LOG) === '1' } type PrinterDebugSnapshot = { when: number reason: string printerType: PrinterType | '' driverKey: string dataBytes?: number deviceFingerprint: string availableTypes: PrinterType[] bluetoothConnection: ReturnType builtin: { uposSupported: boolean uposOptions: ReturnType keywordMatched: boolean virtualBtLinked: boolean uposWillTry: boolean sendChannelHint: string tcpPluginAvailable: boolean savedPort: number } nativeFastPrinter: any /** 用户在蓝牙页是否已确认内置 TSC/TSPL(无法自动检测机芯) */ builtinTscUserAcknowledged: boolean } export async function getPrinterDebugSnapshot (options?: { reason?: string dataBytes?: number }): Promise { const reason = String(options?.reason || 'debug') const printerType = getPrinterType() const driverKey = getCurrentPrinterDriverKey() const deviceFingerprint = getDeviceFingerprint() const availableTypes = getAvailablePrinterTypes() const bluetoothConnection = getBluetoothConnection() const uposOptions = getUposOptionsFromStorage() const keywordMatched = !!deviceFingerprint && (deviceFingerprint.includes('rk3568') || deviceFingerprint.includes('aoa_rk3568')) const virtualBtLinked = !!bluetoothConnection && String(bluetoothConnection.deviceName || '').toLowerCase().includes('virtual bt') const isD320faxDevice = !!deviceFingerprint && (deviceFingerprint.includes('d320fax') || deviceFingerprint.includes('msm8953')) const uposSupported = isNativeUposPrintSupported() const uposWillTry = uposSupported && ( uposOptions.force || (keywordMatched && !isD320faxDevice) || (virtualBtLinked && !isD320faxDevice) || (printerType === 'builtin' && !isD320faxDevice) ) let tcpPluginAvailable = false try { const u = uni as any tcpPluginAvailable = !!(u?.requireNativePlugin && u.requireNativePlugin('moe-tcp-client')) } catch (_) { tcpPluginAvailable = false } const savedPort = parseInt(String(uni.getStorageSync(STORAGE_BUILTIN_PORT) || '0')) || 0 let nativeFastPrinter: any = null try { nativeFastPrinter = await getNativeFastPrinterDebugInfo() } catch (e: any) { nativeFastPrinter = { error: e?.message || String(e || 'getDebugInfo failed') } } return { when: Date.now(), reason, printerType, driverKey, dataBytes: options?.dataBytes, deviceFingerprint, availableTypes, bluetoothConnection, builtin: { uposSupported, uposOptions, keywordMatched, virtualBtLinked, uposWillTry, sendChannelHint: resolvePrintSendChannelHint(), tcpPluginAvailable, savedPort, }, nativeFastPrinter, builtinTscUserAcknowledged: hasUserAcknowledgedBuiltinTsc(), } } export async function logPrinterDebug (options?: { reason?: string dataBytes?: number }): Promise { if (!isPrinterDebugLogEnabled()) return const snapshot = await getPrinterDebugSnapshot(options) try { console.log('[printer-debug] snapshot:', snapshot) } catch (_) {} } const BLE_MTU_DEFAULT = 20 function hasBleWriteProperty (item: any): boolean { const w = item.properties?.write return w === true || w === 'true' } function hasBleWriteNoResponseProperty (item: any): boolean { const p = item.properties || {} return ( p.writeNoResponse === true || p.writeNoResponse === 'true' || p.writeWithoutResponse === true || p.writeWithoutResponse === 'true' ) } /** * Nordic 白名单:GATT 可能只报 write,但机芯数据口仍须 writeNoResponse(长任务带响应写易 mid-job 10007)。 * 非白名单:按 GATT 属性选择。 */ function pickBleWriteUsesNoResponse (serviceId: string, item: any): boolean { const cid = String(item.uuid || '') const hw = hasBleWriteProperty(item) const hn = hasBleWriteNoResponseProperty(item) if (blePairRequiresWriteNoResponse(serviceId, cid)) { return hw || hn } if (!hn) return false if (!hw) return true return false } function listBleCharacteristics ( deviceId: string, serviceId: string, ): Promise }>> { return new Promise((resolve) => { uni.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (charRes) => resolve((charRes.characteristics || []) as Array<{ uuid?: string; properties?: Record }>), fail: () => resolve([]), }) }) } function resolveBleWriteTargetFromChars ( serviceId: string, chars: Array<{ uuid?: string; properties?: Record }>, ): { serviceId: string; characteristicId: string; bleWriteUsesNoResponse: boolean } | null { const writable = (item: { properties?: Record }) => hasBleWriteProperty(item) || hasBleWriteNoResponseProperty(item) for (const item of chars) { const cid = String(item.uuid || '') if (blePairRequiresWriteNoResponse(serviceId, cid) && writable(item)) { return { serviceId, characteristicId: cid, bleWriteUsesNoResponse: pickBleWriteUsesNoResponse(serviceId, item), } } } return null } function resolveBleGenericWriteFromChars ( serviceId: string, chars: Array<{ uuid?: string; properties?: Record }>, ): { serviceId: string; characteristicId: string; bleWriteUsesNoResponse: boolean } | null { const withResp = chars.find(hasBleWriteProperty) const noResp = chars.find(hasBleWriteNoResponseProperty) const target = withResp || noResp if (!target) return null return { serviceId, characteristicId: String(target.uuid || ''), bleWriteUsesNoResponse: pickBleWriteUsesNoResponse(serviceId, target), } } /** 连接/打印前重新发现可写特征,避免 storage 里仍是错误写入方式 */ export function discoverBleWriteCharacteristic (deviceId: string): Promise<{ serviceId: string characteristicId: string bleWriteUsesNoResponse: boolean } | null> { return new Promise((resolve) => { uni.getBLEDeviceServices({ deviceId, success: async (serviceRes) => { const services = serviceRes.services || [] try { /** 第一轮:只认 Nordic 打印串口,避免误选其它服务的可写特征导致 property not support */ for (const svc of services) { const serviceId = String(svc.uuid || '') const chars = await listBleCharacteristics(deviceId, serviceId) const nordic = resolveBleWriteTargetFromChars(serviceId, chars) if (nordic) { console.log('[BLE] discover: nordic uart', nordic) resolve(nordic) return } } for (const svc of services) { const serviceId = String(svc.uuid || '') const chars = await listBleCharacteristics(deviceId, serviceId) const generic = resolveBleGenericWriteFromChars(serviceId, chars) if (generic) { console.log('[BLE] discover: generic writable', generic) resolve(generic) return } } resolve(null) } catch { resolve(null) } }, fail: () => resolve(null), }) }) } async function refreshBlePrintTarget (deviceId: string, driverKey: string): Promise<{ serviceId: string characteristicId: string bleWriteUsesNoResponse: boolean } | null> { const write = await discoverBleWriteCharacteristic(deviceId) if (!write) return null const driver = getPrinterDriverByKey(driverKey) await ensureBleUartNotifyIfNeeded(deviceId, write.serviceId, write.characteristicId) const negotiatedMtu = await requestBleMtuNegotiation(deviceId, driver.preferredBleMtu || BLE_MTU_DEFAULT) setBluetoothConnection({ deviceId, deviceName: uni.getStorageSync(STORAGE_BT_DEVICE_NAME) || 'Bluetooth Printer', serviceId: write.serviceId, characteristicId: write.characteristicId, deviceType: 'ble', mtu: negotiatedMtu, driverKey, bleWriteUsesNoResponse: blePairRequiresWriteNoResponse(write.serviceId, write.characteristicId) ? true : write.bleWriteUsesNoResponse, }) return write } export function getPrinterType (): PrinterType | '' { const type = (uni.getStorageSync(STORAGE_PRINTER_TYPE) as PrinterType) || '' if (!type) return '' if (getAvailablePrinterTypes().includes(type)) return type clearPrinter() return '' } export function getCurrentPrinterDriverKey (): string { return String(uni.getStorageSync(STORAGE_PRINTER_DRIVER_KEY) || '') } export function getBluetoothConnection (): { deviceId: string deviceName: string serviceId: string characteristicId: string deviceType: BtDeviceType transportMode: 'native-plugin' | 'generic' mtu: number bleWriteUsesNoResponse: boolean } | null { const deviceId = uni.getStorageSync(STORAGE_BT_DEVICE_ID) const deviceType = (uni.getStorageSync(STORAGE_BT_DEVICE_TYPE) as BtDeviceType) || 'ble' const transportMode = (uni.getStorageSync(STORAGE_BT_TRANSPORT_MODE) as 'native-plugin' | 'generic') || 'generic' if (!deviceId) return null if (deviceType === 'classic') { /** * 必须原样返回 STORAGE_BT_TRANSPORT_MODE(即上面的 transportMode)。 * 此前误把 native-plugin 读时改回 generic,导致「永远不走安卓基座 / printCommandBytes」, * 一体机只能走 JS 经典蓝牙,极慢且易卡在 31% 等进度。 */ return { deviceId, deviceName: uni.getStorageSync(STORAGE_BT_DEVICE_NAME) || 'Printer', serviceId: '', characteristicId: '', deviceType: 'classic', transportMode, mtu: Number(uni.getStorageSync(STORAGE_BLE_MTU)) || BLE_MTU_DEFAULT, bleWriteUsesNoResponse: false, } } const serviceId = uni.getStorageSync(STORAGE_BT_SERVICE_ID) const characteristicId = uni.getStorageSync(STORAGE_BT_CHARACTERISTIC_ID) if (!serviceId || !characteristicId) return null return { deviceId, deviceName: uni.getStorageSync(STORAGE_BT_DEVICE_NAME) || 'Printer', serviceId, characteristicId, deviceType: 'ble', transportMode, mtu: Number(uni.getStorageSync(STORAGE_BLE_MTU)) || BLE_MTU_DEFAULT, bleWriteUsesNoResponse: uni.getStorageSync(STORAGE_BLE_WRITE_NO_RESPONSE) === '1', } } /** * 经典蓝牙且当前为 generic 时,在插件与 printCommandBytes 可用时尝试 connectNativeFastPrinter 并写入 native-plugin, * 避免仍显示已连蓝牙但实际全程 JS 分包下发(极慢、易卡进度)。 */ export async function ensureNativeClassicTransportIfPossible (): Promise { const conn = getBluetoothConnection() if (!conn || conn.deviceType !== 'classic') return false if (conn.transportMode === 'native-plugin') return true if (!isNativeFastPrinterAvailable() || !isNativePrintCommandBytesSupported()) return false const driverKey = getCurrentPrinterDriverKey() const name = String(conn.deviceName || '').toLowerCase() const preferNative = driverKey === 'd320fax' || name.includes('virtual bt') || name.includes('gprinter') || name.includes('d320') if (!preferNative) return false try { await connectNativeFastPrinter({ deviceId: conn.deviceId, deviceName: conn.deviceName || '', }) setBluetoothConnection({ deviceId: conn.deviceId, deviceName: conn.deviceName, serviceId: conn.serviceId, characteristicId: conn.characteristicId, deviceType: 'classic', transportMode: 'native-plugin', mtu: conn.mtu, driverKey: getCurrentPrinterDriverKey(), }) return true } catch (e) { console.warn('[printer] ensureNativeClassicTransportIfPossible failed', e) return false } } /** * 经典蓝牙已走 native-fast-printer 基座链路(常见:一体机「Virtual BT Printer」/ 佳博 SDK)。 * 预览与打印日志重打在此模式下应走原生 printTemplate;否则走 BLE 或 JS 经典蓝牙光栅/直发 TSC。 */ export function isNativeBaseClassicBluetoothTransport (): boolean { const conn = getBluetoothConnection() return conn?.deviceType === 'classic' && conn?.transportMode === 'native-plugin' } export function isBuiltinConnected (): boolean { return getPrinterType() === 'builtin' } export function isBuiltinPrinterAvailable (): boolean { // #ifdef APP-PLUS try { const plugin = (uni as any)?.requireNativePlugin ? (uni as any).requireNativePlugin('moe-tcp-client') : null return !!plugin } catch (_) { return false } // #endif // #ifndef APP-PLUS return false // #endif } export { isAoaAioDevice } from '../deviceInfo' export function isBuiltinPrinterEnabledByDeviceModel (): boolean { const fingerprint = getDeviceFingerprint() if (!fingerprint) return false return BUILTIN_PRINTER_DEVICE_KEYWORDS.some(keyword => fingerprint.includes(String(keyword || '').toLowerCase())) } /** * 一体机(AIO)上若当前连的是 Virtual BT Printer,打印改走内置 ESC 光栅(与预览 Canvas 一致,避免 JSON 直打空白)。 */ export function isAioBuiltinPrintCapable (): boolean { return isBuiltinPrinterAvailable() || isNativeUposPrintSupported() } /** Gprinter D320FAX 一体机(Qualcomm msm8953):UPOS 串口不可用,走佳博 SDK + Virtual BT TSC */ export function isD320faxAioDevice (): boolean { const fp = getDeviceFingerprint().toLowerCase() return fp.includes('d320fax') || fp.includes('msm8953') } /** D320FAX 误切 builtin 后恢复 bluetooth + native-plugin,避免 UPOS 假成功 */ export function restoreD320faxVirtualBtBluetoothMode (): void { if (!isD320faxAioDevice() || !isVirtualBtBluetoothConnection()) return const conn = getBluetoothConnection() if (!conn || getPrinterType() !== 'builtin') return setBluetoothConnection({ deviceId: conn.deviceId, deviceName: conn.deviceName, serviceId: conn.serviceId, characteristicId: conn.characteristicId, deviceType: conn.deviceType, transportMode: conn.transportMode || 'native-plugin', mtu: conn.mtu, driverKey: getCurrentPrinterDriverKey() || 'd320fax', }) } /** 当前打印字节预计走哪条物理通道(供 Printer 页 Debug 展示) */ export function resolvePrintSendChannelHint (): string { const type = getPrinterType() const virtualBt = isVirtualBtBluetoothConnection() const conn = getBluetoothConnection() const uposSupported = isNativeUposPrintSupported() const uposPrefer = getUposOptionsFromStorage().prefer || 'builtin' if (virtualBt) { if (conn?.transportMode === 'native-plugin' && isNativePrintCommandBytesSupported()) { return 'Gprinter SDK TSC (Virtual BT)' } return 'Bluetooth classic TSC (Virtual BT)' } if (type === 'builtin') { if (uposSupported) return `UPOS/${uposPrefer}` if (isBuiltinPrinterAvailable()) return 'TCP localhost' return 'builtin-unavailable' } if (type === 'bluetooth') { if (conn?.deviceType === 'ble') { return 'Bluetooth BLE (Canvas→TSC raster)' } if (conn?.transportMode === 'native-plugin' && isNativePrintCommandBytesSupported()) { return 'Bluetooth printCommandBytes (Gprinter SDK)' } if (conn?.deviceType === 'classic') return 'Bluetooth classic JS' return 'Bluetooth' } return 'none' } /** 当前经典蓝牙是否连到一体机 Virtual BT(虚拟 SPP 别名) */ export function isVirtualBtBluetoothConnection (): boolean { const conn = getBluetoothConnection() if (!conn) return false return String(conn.deviceName || '').toLowerCase().includes('virtual bt') } export function preferBuiltinPrinterOnAioDevice (): boolean { restoreD320faxVirtualBtBluetoothMode() if (getPrinterType() === 'builtin') { if (isVirtualBtBluetoothConnection()) return false return true } if (!isAioBuiltinPrintCapable()) return false /** 一体机已连 Virtual BT:保持 Bluetooth + 佳博 TSC,勿切 Built-in/UPOS */ if (isVirtualBtBluetoothConnection()) { return false } if (!isBuiltinPrinterEnabledByDeviceModel()) return false setBuiltinPrinter(getCurrentPrinterDriverKey() || 'generic-tsc') return true } export function getAvailablePrinterTypes (): PrinterType[] { if (isAioBuiltinPrintCapable() && (isBuiltinPrinterEnabledByDeviceModel() || !!getBluetoothConnection())) { // AIO 机型同时保留蓝牙入口,避免只能走内置打印导致无法连接 Virtual BT Printer。 return ['bluetooth', 'builtin'] } return ['bluetooth'] } function buildClassicBluetoothError (message: string, deviceId?: string): Error { const baseMessage = String(message || 'Classic Bluetooth error') if (!classicBluetooth || typeof classicBluetooth.getDebugState !== 'function') { return new Error(baseMessage) } try { const debugState = classicBluetooth.getDebugState() || {} const details: string[] = [] if (deviceId) details.push(`device=${deviceId}`) if (debugState.lastSocketStrategy) details.push(`socket=${debugState.lastSocketStrategy}`) if (debugState.connectionState) details.push(`state=${debugState.connectionState}`) if (typeof debugState.socketConnected === 'boolean') details.push(`connected=${debugState.socketConnected}`) if (typeof debugState.outputReady === 'boolean') details.push(`outputReady=${debugState.outputReady}`) if (debugState.lastSendMode) details.push(`sendMode=${debugState.lastSendMode}`) if (debugState.lastSendError) details.push(`sendError=${debugState.lastSendError}`) else if (debugState.lastError) details.push(`lastError=${debugState.lastError}`) if (details.length === 0) return new Error(baseMessage) return new Error(`${baseMessage}\n${details.join('\n')}`) } catch (_) { return new Error(baseMessage) } } /** * 发送打印数据到当前已选打印机 * @param data 字节数组(TSC 指令) * @param onProgress 可选进度回调 0~100 */ export function sendToPrinter ( data: number[], onProgress?: (percent: number) => void ): Promise { void logPrinterDebug({ reason: 'sendToPrinter', dataBytes: data?.length || 0 }) const type = getPrinterType() printRunDiag('sendToPrinter', { type, dataBytes: data?.length || 0 }) /** Virtual BT:TSC 光栅必须走佳博 SDK / 经典蓝牙 SPP,禁止误投 UPOS(TSC 字节 UPOS 不认会卡死) */ if (isVirtualBtBluetoothConnection()) { return sendViaVirtualBtTsc(data, onProgress) } if (type === 'bluetooth') { const conn = getBluetoothConnection() if (conn && conn.deviceType === 'classic') { if (conn.transportMode === 'native-plugin' && isNativePrintCommandBytesSupported()) { return sendViaNativeClassicPlugin(data, onProgress).catch((err) => { console.warn('[printer] native printCommandBytes failed, fallback JS classic', err) return sendViaClassic(data, onProgress) }) } return sendViaClassic(data, onProgress) } return sendViaBle(data, onProgress) } if (type === 'builtin') { return sendViaBuiltin(data) } return Promise.reject(new Error('No printer connected. Please connect a Bluetooth or built-in printer first.')) } /** 与打印机页扫描/连接一致:未 open 适配器时 writeBLECharacteristicValue 报 fail not init */ function bleOpenAdapter (): Promise { return new Promise((resolve, reject) => { // #ifdef APP-PLUS uni.openBluetoothAdapter({ success: () => resolve(), fail: (err: any) => { const msg = String(err?.errMsg || '') const code = err?.errCode if (msg.includes('already') || code === 10001) resolve() else reject(new Error(msg || 'openBluetoothAdapter failed')) }, }) // #endif // #ifndef APP-PLUS resolve() // #endif }) } /** 设置页 onUnmounted 可能已 closeBluetoothAdapter,需重新建链后才能写特征值 */ function bleEnsureDeviceConnected (deviceId: string): Promise { return new Promise((resolve, reject) => { // #ifdef APP-PLUS uni.createBLEConnection({ deviceId, timeout: 15000, success: () => resolve(), fail: (err: any) => { const msg = String(err?.errMsg || '') const code = err?.errCode if ( code === -1 || msg.includes('already') || msg.includes('Connected') || msg.includes('connected') || msg.includes('已连接') ) { resolve() return } reject(new Error(msg || 'createBLEConnection failed')) }, }) // #endif // #ifndef APP-PLUS resolve() // #endif }) } /** * 每次打印前会 createBLEConnection,链路 MTU 可能回到默认 23;若仍按 storage 里 512 分包,实机常丢数据但 write 仍 success。 */ let bleNordicUartValueListenerAttached = false function attachBleNordicUartValueListener (): void { if (bleNordicUartValueListenerAttached) return bleNordicUartValueListenerAttached = true try { if (typeof uni.onBLECharacteristicValueChange === 'function') { uni.onBLECharacteristicValueChange(() => {}) } } catch (_) {} } /** * Nordic UART 类标签机:未对 TX 打开 notify 时,部分安卓栈第二包起对 RX 写会统一报 property not support (10007)。 * 连接后、大批量 write 前各调用一次(幂等)。 */ export function ensureBleUartNotifyIfNeeded ( deviceId: string, serviceId: string, rxCharacteristicId?: string ): Promise { // #ifndef APP-PLUS return Promise.resolve() // #endif // #ifdef APP-PLUS if (!isNordicUartStyleBleService(serviceId)) { return Promise.resolve() } const rx = normalizeBleUuid(rxCharacteristicId || '') return new Promise((resolve) => { uni.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) => { const chars = ((res as any).characteristics || []) as Array<{ uuid?: string; properties?: Record }> const notifyChar = chars.find((c) => { const p = c.properties || {} const n = p.notify === true || p.notify === 'true' || p.indicate === true || p.indicate === 'true' if (!n) return false const cid = normalizeBleUuid(String(c.uuid || '')) if (rx && cid === rx) return false return true }) if (!notifyChar?.uuid) { console.warn('[BLE] Nordic 串口:未找到可订阅的 notify/indicate 特征,跳过后续写入可能仍 10007') resolve() return } const cid = String(notifyChar.uuid) uni.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId: cid, state: true, success: () => { attachBleNordicUartValueListener() console.log('[BLE] Nordic 串口已订阅 notify:', cid) setTimeout(() => resolve(), 80) }, fail: () => { resolve() }, }) }, fail: () => resolve(), }) }) // #endif } function requestBleMtuNegotiation (deviceId: string, preferredMtu: number): Promise { return new Promise((resolve) => { // #ifdef APP-PLUS const targetMtu = Math.max(20, Math.min(512, Math.round(preferredMtu || 20))) if (targetMtu <= 20 || typeof (uni as any).setBLEMTU !== 'function') { resolve(20) return } let settled = false const done = (value: number) => { if (settled) return settled = true clearTimeout(timer) const m = Math.max(20, Math.round(value || 20)) try { uni.setStorageSync(STORAGE_BLE_MTU, m) } catch (_) {} resolve(m) } const timer = setTimeout(() => done(20), 3000) ;(uni as any).setBLEMTU({ deviceId, mtu: targetMtu, success: (res: any) => done(Number(res?.mtu) || targetMtu), fail: () => done(20), }) // #endif // #ifndef APP-PLUS resolve(20) // #endif }) } function sendViaBle ( data: number[], onProgress?: (percent: number) => void ): Promise { const conn = getBluetoothConnection() if (!conn) { return Promise.reject(new Error('Bluetooth printer not connected.')) } const deviceId = conn.deviceId let serviceId = conn.serviceId let characteristicId = conn.characteristicId let bleWriteUsesNoResponse = conn.bleWriteUsesNoResponse const runWritesWithPayloadSize = (payloadSize: number): Promise => { const chunks: number[][] = [] for (let i = 0; i < data.length; i += payloadSize) { chunks.push(data.slice(i, i + payloadSize)) } const total = chunks.length let sent = 0 let completed = false let timeoutId: ReturnType | null = setTimeout(() => {}, 0) /** Nordic 大包连续 0ms 间隔时,部分机型第二包起 10007;与 enable notify 配合 */ const nordicUart = isNordicUartStyleBleService(serviceId) const writeDelayMs = nordicUart && payloadSize >= 64 ? 18 : payloadSize >= 180 ? 0 : payloadSize > 20 ? 2 : 10 /** 佳博 Nordic 白名单:无视 storage 里旧的 false,长任务必须 writeNoResponse */ const blePairForceNoResponse = blePairRequiresWriteNoResponse(serviceId, characteristicId) let effectiveUseNoResp = blePairForceNoResponse ? true : bleWriteUsesNoResponse let hasFlippedWriteModeThisJob = false let pendingPersistUseNoResp: boolean | null = null const resetTimeout = (reject: (reason?: any) => void) => { if (timeoutId) clearTimeout(timeoutId) timeoutId = setTimeout(() => { if (completed) return completed = true reject(new Error('BLE write timeout')) }, Math.max(60000, total * 500)) } function logBleWriteFail (err: any, useNoResp: boolean, bufferLen: number) { const msg = String(err?.errMsg ?? err?.message ?? '') let errSerialized = '' try { errSerialized = JSON.stringify(err) } catch { errSerialized = String(err) } console.error('[sendViaBle] writeBLECharacteristicValue fail — 完整信息供真机调试复制', { errMsg: msg, errCode: err?.errCode, errno: err?.errno, code: err?.code, writeTypeUsed: useNoResp ? 'writeNoResponse' : 'write', effectiveUseNoResp, bleWriteUsesNoResponseSaved: bleWriteUsesNoResponse, bufferLen, sentIndex: sent, totalChunks: total, serviceId, characteristicId, deviceId, rawErr: err, errSerialized, blePairForcedNoResponse: blePairForceNoResponse, }) } function writeOneBuffer (buffer: ArrayBuffer): Promise { return new Promise((resolve, reject) => { const tryWrite = (useNoResp: boolean, allowFlip: boolean) => { const opts: UniApp.WriteBLECharacteristicValueOption = { deviceId, serviceId, characteristicId, value: buffer, } /** 无响应写 / 带响应写均显式声明 writeType,兼容新版 Android BLE 栈 */ opts.writeType = useNoResp ? 'writeNoResponse' : 'write' uni.writeBLECharacteristicValue({ ...opts, success: () => { if (pendingPersistUseNoResp != null) { try { uni.setStorageSync( PrinterStorageKeys.bleWriteNoResponse, pendingPersistUseNoResp ? '1' : '0' ) } catch (_) {} pendingPersistUseNoResp = null } resolve() }, fail: (err: any) => { const msg = String(err?.errMsg ?? err?.message ?? '') logBleWriteFail(err, useNoResp, buffer.byteLength) const notSupport = msg.includes('property not support') || msg.includes('not support') || String(err?.errCode) === '10007' if (notSupport) { const nextUseNoResp = !useNoResp /** 白名单 mid-job:带响应写跑若干包后 10007 → 立即改 writeNoResponse 重试本包 */ if (blePairForceNoResponse && !useNoResp && nextUseNoResp) { effectiveUseNoResp = true pendingPersistUseNoResp = true console.warn('[sendViaBle] 白名单串口 mid-job write→writeNoResponse 重试', { sentIndex: sent, totalChunks: total, }) tryWrite(true, false) return } /** 白名单已用 writeNoResponse 跑通后,禁止切到带响应写 */ if (blePairForceNoResponse && !nextUseNoResp && sent > 0) { console.warn( '[sendViaBle] 白名单串口已发包后禁止切到带响应写;请保持 writeNoResponse' ) reject(new Error(msg || 'BLE write failed')) return } if (allowFlip && !hasFlippedWriteModeThisJob && sent === 0) { hasFlippedWriteModeThisJob = true effectiveUseNoResp = nextUseNoResp pendingPersistUseNoResp = effectiveUseNoResp console.warn('[sendViaBle] property not support → 首包切换写入方式重试', { nextMode: effectiveUseNoResp ? 'writeNoResponse' : 'write', }) tryWrite(effectiveUseNoResp, false) return } } reject(new Error(msg || 'BLE write failed')) }, }) } tryWrite(effectiveUseNoResp, !hasFlippedWriteModeThisJob) }) } function sendNext (): Promise { if (completed) { return Promise.reject(new Error('BLE write timeout')) } if (sent >= total) { completed = true if (timeoutId) clearTimeout(timeoutId) /** 末包 write 成功后立刻 resolve 时,部分机芯尚未吃完缓冲;短延迟再结束,减少「界面成功但不出纸」 */ const settleMs = data.length > 400 ? 180 : 50 return new Promise((resolve) => { setTimeout(() => { if (onProgress) onProgress(100) resolve() }, settleMs) }) } const chunk = chunks[sent] const buffer = new ArrayBuffer(chunk.length) const view = new DataView(buffer) for (let j = 0; j < chunk.length; j++) { view.setUint8(j, chunk[j] & 0xff) } return new Promise((resolve, reject) => { resetTimeout(reject) writeOneBuffer(buffer) .then(() => { if (completed) return sent++ if (onProgress) onProgress(Math.round((sent / total) * 100)) if (writeDelayMs <= 0) { sendNext().then(resolve).catch(reject) return } setTimeout(() => sendNext().then(resolve).catch(reject), writeDelayMs) }) .catch((e: any) => { if (completed) return completed = true if (timeoutId) clearTimeout(timeoutId) reject(e instanceof Error ? e : new Error(String(e?.message || e || 'BLE write failed'))) }) }) } return sendNext() } /** * 单包 ATT 可写字节数:理论为 mtu-3;MTU≤23 时旧代码误用 mtu 本值(如 23)会超过链路真上限 20,导致截断/无出纸却 write success。 * 协商到 512 时不少佳博/安卓组合仍不能稳定传 500+ 字节/包,需再压到安全上限。 */ const mtuToPayloadSize = (negotiatedMtu: number) => { const mtu = Math.max(23, Math.min(512, Math.round(negotiatedMtu || 23))) const attPayload = Math.max(20, mtu - 3) const SAFE_BLE_WRITE_CAP = 182 return Math.min(attPayload, SAFE_BLE_WRITE_CAP) } // #ifdef APP-PLUS if (conn.deviceType === 'ble') { const driverKey = getCurrentPrinterDriverKey() const driver = getPrinterDriverByKey(driverKey) const nordicLikely = isNordicUartStyleBleService(serviceId) return bleOpenAdapter() .then(() => bleEnsureDeviceConnected(deviceId)) .then(() => new Promise((r) => setTimeout(r, nordicLikely ? 200 : 100))) .then(() => refreshBlePrintTarget(deviceId, driverKey || driver.key)) .then(() => { const latest = getBluetoothConnection() if (latest) { serviceId = latest.serviceId characteristicId = latest.characteristicId bleWriteUsesNoResponse = latest.bleWriteUsesNoResponse } const mtu = latest?.mtu || driver.preferredBleMtu || BLE_MTU_DEFAULT return runWritesWithPayloadSize(mtuToPayloadSize(mtu)) }) } // #endif return runWritesWithPayloadSize(mtuToPayloadSize(conn.mtu || BLE_MTU_DEFAULT)) } /** 大包/虚拟蓝牙写入慢:按字节量拉长等待,避免 JS 已超时拒绝但底层仍在写出(纸已出、接口 9 未落库) */ export function estimateClassicSendTimeoutMs (byteLength: number): number { const n = Math.max(0, Math.floor(byteLength || 0)) const base = 90000 const perByte = Math.floor(n / 400) return Math.min(600000, Math.max(60000, base + perByte)) } function numberArrayToBase64 (data: number[]): string { const u8 = new Uint8Array(data.length) for (let i = 0; i < data.length; i++) u8[i] = data[i] & 0xff try { const u = uni as any if (typeof u.arrayBufferToBase64 === 'function') { return u.arrayBufferToBase64(u8.buffer) } } catch (_) {} let binary = '' for (let i = 0; i < u8.length; i++) binary += String.fromCharCode(u8[i]) if (typeof btoa !== 'undefined') return btoa(binary) return '' } /** * Virtual BT(含 D320FAX / AOA 一体机):Canvas 光栅 TSC 指令 → 佳博 SDK printCommandBytes 或经典蓝牙 SPP。 */ async function sendViaVirtualBtTsc ( data: number[], onProgress?: (percent: number) => void, ): Promise { printRunDiag('sendToPrinter_virtual_bt_tsc', { dataBytes: data?.length || 0 }) await ensureNativeClassicTransportIfPossible() const conn = getBluetoothConnection() if (conn?.transportMode === 'native-plugin' && isNativePrintCommandBytesSupported()) { const nativeWaitMs = Math.min( 600000, Math.max(60000, estimateClassicSendTimeoutMs(data.length)), ) try { await sendViaNativeClassicPlugin(data, onProgress, { writeTimeoutMs: nativeWaitMs }) return } catch (e) { printRunDiag('virtual_bt_native_bytes_fallback_js', { err: String((e as Error)?.message || e), dataBytes: data.length, }) try { if (classicBluetooth?.ensureConnection) { classicBluetooth.ensureConnection(conn.deviceId) } } catch (_) {} return sendViaClassic(data, onProgress) } } if (conn?.deviceType === 'classic') { printRunDiag('sendToPrinter_virtual_bt_classic_js', { dataBytes: data?.length || 0 }) return sendViaClassic(data, onProgress) } return Promise.reject(new Error('Virtual BT printer not connected.')) } /** @deprecated 使用 sendViaVirtualBtTsc;保留别名兼容旧诊断关键字 */ async function sendViaD320faxVirtualBt ( data: number[], onProgress?: (percent: number) => void, ): Promise { return sendViaVirtualBtTsc(data, onProgress) } /** * 经典蓝牙已走 native-fast-printer 佳博 SDK 时,整页光栅字节经 printCommandBytes 下发,避免 JS 蓝牙慢发。 */ function sendViaNativeClassicPlugin ( data: number[], onProgress?: (percent: number) => void, options: { writeTimeoutMs?: number } = {}, ): Promise { const conn = getBluetoothConnection() if (!conn || conn.deviceType !== 'classic' || conn.transportMode !== 'native-plugin') { return Promise.reject(new Error('NATIVE_CLASSIC_TRANSPORT_NOT_ACTIVE')) } if (!isNativePrintCommandBytesSupported()) { return Promise.reject(new Error('NATIVE_PRINT_COMMAND_BYTES_NOT_SUPPORTED')) } const base64 = numberArrayToBase64(data) if (!base64) { return Promise.reject(new Error('BASE64_ENCODE_FAILED')) } const writeTimeoutMs = options.writeTimeoutMs ?? Math.min(600000, Math.max(45000, estimateClassicSendTimeoutMs(data.length))) if (onProgress) onProgress(5) let waitVisual = 5 const waitKeepAlive = onProgress ? setInterval(() => { if (waitVisual >= 95) return waitVisual = Math.min(95, waitVisual + 1) onProgress(waitVisual) }, 1200) : null return printNativeCommandBytes({ deviceId: conn.deviceId, deviceName: conn.deviceName, base64, }).then(async () => { printRunDiag('native_printCommandBytes_queued', { dataBytes: data.length }) try { const verified = await waitForNativeCommandBytesWriteComplete(writeTimeoutMs, (waitPct) => { if (onProgress) { const p = Math.max(waitVisual, 8 + Math.round(waitPct * 0.9)) waitVisual = p onProgress(p) } }) printRunDiag('native_printCommandBytes_verified', { writeMs: verified.writeMs, commandBytes: verified.commandBytes, stage: verified.stage, }) if (onProgress) onProgress(100) } finally { if (waitKeepAlive) clearInterval(waitKeepAlive) } }) } function sendViaClassic ( data: number[], onProgress?: (percent: number) => void ): Promise { // #ifdef APP-PLUS const conn = getBluetoothConnection() if (!conn || conn.deviceType !== 'classic') { return Promise.reject(new Error('Classic Bluetooth printer not connected.')) } const sendData = data.map((byte) => { const value = byte & 0xff return value >= 128 ? value - 256 : value }) const sendTimeoutMs = estimateClassicSendTimeoutMs(sendData.length) return new Promise((resolve, reject) => { let settled = false const finish = (fn: () => void) => { if (settled) return settled = true clearTimeout(timeoutId) fn() } const timeoutId = setTimeout(() => { finish(() => { reject(buildClassicBluetoothError('Classic Bluetooth send timeout', conn.deviceId)) }) }, sendTimeoutMs + 25000) try { if (!classicBluetooth) { finish(() => reject(new Error('Classic Bluetooth not available'))) return } const isReady = () => { const debugState = typeof classicBluetooth.getDebugState === 'function' ? classicBluetooth.getDebugState() : null const connectionState = String(debugState?.connectionState || '').trim().toLowerCase() const ready = debugState ? (!!debugState.outputReady && (!!debugState.socketConnected || connectionState === 'connected')) : true return { ready, debugState } } const sendNow = () => { if (typeof classicBluetooth.sendByteDataAsync === 'function') { let callbackSettled = false const asyncTimeoutTimer = setTimeout(() => { if (callbackSettled) return callbackSettled = true finish(() => reject(buildClassicBluetoothError('Classic Bluetooth async send timeout', conn.deviceId))) }, sendTimeoutMs) const started = classicBluetooth.sendByteDataAsync( sendData, (ok: boolean, errorMessage?: string) => { callbackSettled = true clearTimeout(asyncTimeoutTimer) finish(() => { if (onProgress) onProgress(100) if (ok) { resolve() return } reject(buildClassicBluetoothError( errorMessage || classicBluetooth.getLastError?.() || 'Classic Bluetooth send failed', conn.deviceId )) }) }, (chunkPct: number) => { if (onProgress && typeof chunkPct === 'number') { try { onProgress(Math.max(0, Math.min(99, Math.floor(chunkPct)))) } catch (_) {} } }, ) if (started === false) { clearTimeout(asyncTimeoutTimer) finish(() => reject(buildClassicBluetoothError('Classic Bluetooth async send start failed', conn.deviceId))) } return } finish(() => reject(buildClassicBluetoothError('Classic Bluetooth async API missing', conn.deviceId))) } const waitReadyAndSend = (startMs: number) => { const { ready } = isReady() if (ready) { sendNow() return } if (Date.now() - startMs > 1500) { const errorMessage = typeof classicBluetooth.getLastError === 'function' ? classicBluetooth.getLastError() : '' finish(() => reject(buildClassicBluetoothError(errorMessage || 'Classic Bluetooth connection is not ready', conn.deviceId))) return } setTimeout(() => waitReadyAndSend(startMs), 120) } /** * 新型号/部分安卓机:连接建立慢,或页面切换后 socket 丢失但 UI 仍显示已连。 * 发送前补连一次,再等短时间就绪。 */ try { const { ready } = isReady() if (!ready && typeof classicBluetooth.ensureConnection === 'function') { classicBluetooth.ensureConnection(conn.deviceId) } } catch (_) {} waitReadyAndSend(Date.now()) } catch (e: any) { finish(() => reject(buildClassicBluetoothError(e?.message || String(e || 'Classic Bluetooth send exception'), conn.deviceId))) } }) // #endif // #ifndef APP-PLUS return Promise.reject(new Error('Classic Bluetooth is only available in the app.')) // #endif } function sendViaBuiltin (data: number[]): Promise { // #ifdef APP-PLUS try { const sendViaBuiltinTcpLocalhost = (): Promise => { const u = uni as any const moeTcp = u.requireNativePlugin ? u.requireNativePlugin('moe-tcp-client') : null if (!moeTcp) { return Promise.reject(new Error('BUILTIN_PLUGIN_NOT_FOUND')) } const uint8 = new Uint8Array(data.length) for (let i = 0; i < data.length; i++) uint8[i] = data[i] & 0xff const hexStr = Array.from(uint8) .map(b => ('0' + (b & 0xff).toString(16)).slice(-2)) .join('') printRunDiag('builtin_tcp_hex_ready', { dataBytes: data.length, hexLen: hexStr.length }) const savedPort = parseInt(uni.getStorageSync(STORAGE_BUILTIN_PORT)) || 0 const ports = savedPort > 0 ? [savedPort, ...BUILTIN_PROBE_PORTS.filter(p => p !== savedPort)] : [...BUILTIN_PROBE_PORTS] function tryPort (idx: number): Promise { if (idx >= ports.length) { return Promise.reject(new Error( 'Built-in printer: all ports failed (tried ' + BUILTIN_PROBE_PORTS.join(', ') + '). Check if the printer service is running on this device.' )) } const port = ports[idx] return new Promise((resolve, reject) => { console.log('[builtin] trying 127.0.0.1:' + port) let settled = false const timeoutId = setTimeout(() => { if (settled) return settled = true console.warn('[builtin] port connect timeout:', port) try { moeTcp.disconnect() } catch (_) {} tryPort(idx + 1).then(resolve).catch(reject) }, 2500) moeTcp.connect({ ip: '127.0.0.1', port }, (res: string) => { if (settled) return settled = true clearTimeout(timeoutId) try { const r = typeof res === 'string' ? JSON.parse(res) : res if (r.code !== 1) { console.log('[builtin] port ' + port + ' failed: ' + (r.msg || '')) try { moeTcp.disconnect() } catch (_) {} tryPort(idx + 1).then(resolve).catch(reject) return } console.log('[builtin] connected on port ' + port) uni.setStorageSync(STORAGE_BUILTIN_PORT, String(port)) moeTcp.sendHexStr({ message: hexStr }) printRunDiag('builtin_tcp_sent', { port, dataBytes: data.length }) setTimeout(() => { try { moeTcp.disconnect() } catch (_) {} resolve() }, 300) } catch (e) { try { moeTcp.disconnect() } catch (_) {} tryPort(idx + 1).then(resolve).catch(reject) } }) }) } return tryPort(0) } // rk3568 类一体机:优先走 UnifiedPOS 内置/串口直打(无需蓝牙配对、无需 localhost 端口服务) const fingerprint = getDeviceFingerprint() const uposOptions = getUposOptionsFromStorage() const isD320faxDevice = isD320faxAioDevice() const matchesKeywords = !!fingerprint && (fingerprint.includes('rk3568') || fingerprint.includes('aoa_rk3568')) const virtualBtLinked = isVirtualBtBluetoothConnection() const uposSupported = isNativeUposPrintSupported() const shouldTryUpos = uposSupported && ( uposOptions.force || (matchesKeywords && !isD320faxDevice) || (getPrinterType() === 'builtin' && !isD320faxDevice && !virtualBtLinked) ) const shouldPreferTcpFirst = (matchesKeywords || isD320faxDevice) && !uposOptions.force && uposOptions.prefer !== 'serial' && !virtualBtLinked console.warn( '[builtin] route decision' + ' fingerprint=' + String(fingerprint || '-') + ' uposSupported=' + String(!!uposSupported) + ' keywordMatched=' + String(!!matchesKeywords) + ' uposForce=' + String(!!uposOptions.force) + ' shouldTryUpos=' + String(!!shouldTryUpos) + ' uposPrefer=' + String(uposOptions.prefer || '-') + ' uposSerialPath=' + String(uposOptions.serialPath || '-') + ' uposBaudrate=' + String(uposOptions.baudrate || '-') + ' dataBytes=' + String(data?.length || 0) ) printRunDiag('builtin_route_decision', { fingerprint: String(fingerprint || '-').slice(0, 96), shouldTryUpos, shouldPreferTcpFirst, dataBytes: data.length, }) const runUposPrint = (): Promise => { const tB64 = Date.now() const base64 = numberArrayToBase64(data) printRunDiag('builtin_base64_encoded', { ms: Date.now() - tB64, dataBytes: data.length, base64Len: base64 ? base64.length : 0, }) if (!base64) { return Promise.reject(new Error('BASE64_ENCODE_FAILED')) } console.warn( '[builtin] using UPOS printUposCommandBytes' + ' prefer=' + String(uposOptions.prefer || '-') + ' serialPath=' + String(uposOptions.serialPath || '-') + ' baudrate=' + String(uposOptions.baudrate || '-') + ' base64Len=' + String(base64.length || 0) ) printRunDiag('builtin_upos_invoke', { prefer: uposOptions.prefer || 'builtin', baudrate: uposOptions.baudrate || 9600, }) /** * 大光栅 ESC/POS 下发可能 >30s;原生 printUposCommandBytes 自身有 300s 超时。 * 切勿再用 18s Promise.race:否则会误判失败并回退 localhost TCP,大包 hex 发送极易卡数分钟且无纸。 */ const tUpos = Date.now() const uposJob = printNativeUposCommandBytes({ base64, prefer: uposOptions.prefer, serialPath: uposOptions.serialPath, baudrate: uposOptions.baudrate, }).then(async () => { printRunDiag('builtin_upos_js_callback_ok', { waitMs: Date.now() - tUpos }) let debugInfo: any = null try { debugInfo = await getNativeFastPrinterDebugInfo() } catch (_) { debugInfo = null } printRunDiag('builtin_upos_debug_after', { stage: String(debugInfo?.stage || '-'), commandBytes: Number(debugInfo?.commandBytes || 0), writeMs: Number(debugInfo?.writeMs || 0), }) const stage = String(debugInfo?.stage || '').toLowerCase() const commandBytes = Number(debugInfo?.commandBytes || 0) const hasWriteMs = Number(debugInfo?.writeMs || 0) > 0 const okStage = stage.includes('printuposcommandbytes:ok') const looksWritten = commandBytes > 0 || hasWriteMs if (!okStage || !looksWritten) { const detail = [ 'UPOS_VERIFY_FAILED', `stage=${String(debugInfo?.stage || '-')}`, `lastError=${String(debugInfo?.lastError || '-')}`, `commandBytes=${String(debugInfo?.commandBytes ?? '-')}`, `writeMs=${String(debugInfo?.writeMs ?? '-')}`, ].join('\n') throw new Error(detail) } }) return uposJob.catch(async (e: any) => { const msg = e instanceof Error ? e.message : String(e || 'UPOS_PRINT_FAILED') printRunDiag('builtin_upos_fail_fallback', { err: msg.slice(0, 240), waitMs: Date.now() - tUpos }) if (isVirtualBtBluetoothConnection()) { try { await sendViaVirtualBtTsc(data, onProgress) return } catch (gErr: any) { const gMsg = gErr instanceof Error ? gErr.message : String(gErr || 'VIRTUAL_BT_TSC_FAILED') throw new Error(`UPOS failed (${msg}); Virtual BT TSC failed (${gMsg})`) } } if (isD320faxAioDevice()) { console.warn('[builtin] D320FAX UPOS failed, fallback localhost TCP', msg) return sendViaBuiltinTcpLocalhost() } console.warn('[builtin] UPOS failed/timeout, fallback localhost TCP', msg) return sendViaBuiltinTcpLocalhost() }) } if (shouldTryUpos && !shouldPreferTcpFirst) { return runUposPrint() } if (shouldPreferTcpFirst) { return sendViaBuiltinTcpLocalhost().catch((tcpErr: any) => { const tcpMsg = tcpErr instanceof Error ? tcpErr.message : String(tcpErr || 'BUILTIN_TCP_FAILED') console.warn('[builtin] localhost TCP failed, fallback UPOS', tcpMsg) if (!shouldTryUpos) { return Promise.reject(new Error(tcpMsg)) } return runUposPrint() }) } return sendViaBuiltinTcpLocalhost() } catch (e) { return Promise.reject(e) } // #endif // #ifndef APP-PLUS return Promise.reject(new Error('Built-in printer is only available in the app. Use Bluetooth printer on this device.')) // #endif }