/** * 打印机连接与下发:蓝牙(BLE) / 一体机(TCP localhost) */ import type { ActiveBtDeviceType, PrinterType } from './types/printer' import classicBluetooth from './bluetoothTool.js' import { getDeviceFingerprint } from '../deviceInfo' 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_BLE_MTU = 'bleMTU' const STORAGE_BUILTIN_PORT = 'builtinPort' const STORAGE_PRINTER_DRIVER_KEY = 'printerDriverKey' const BUILTIN_PROBE_PORTS = [9100, 4000, 9000, 6000] const BUILTIN_PRINTER_DEVICE_KEYWORDS: string[] = [ // 在这里补充需要走 Built-in 的设备型号关键字(小写匹配) // 例如:'desktop-aio', 'pos-terminal-x1' ] 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, bleMTU: STORAGE_BLE_MTU, driverKey: STORAGE_PRINTER_DRIVER_KEY, } 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 mtu?: number driverKey?: string }) { 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_BLE_MTU, info.mtu != null ? info.mtu : BLE_MTU_DEFAULT) uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, info.driverKey || '') } 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_BLE_MTU) uni.removeStorageSync(STORAGE_BUILTIN_PORT) uni.removeStorageSync(STORAGE_PRINTER_DRIVER_KEY) } const BLE_MTU_DEFAULT = 20 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 mtu: number } | null { const deviceId = uni.getStorageSync(STORAGE_BT_DEVICE_ID) const deviceType = (uni.getStorageSync(STORAGE_BT_DEVICE_TYPE) as BtDeviceType) || 'ble' if (!deviceId) return null if (deviceType === 'classic') { return { deviceId, deviceName: uni.getStorageSync(STORAGE_BT_DEVICE_NAME) || 'Printer', serviceId: '', characteristicId: '', deviceType: 'classic', mtu: BLE_MTU_DEFAULT, } } 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', mtu: Number(uni.getStorageSync(STORAGE_BLE_MTU)) || BLE_MTU_DEFAULT, } } 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 function isBuiltinPrinterEnabledByDeviceModel (): boolean { const fingerprint = getDeviceFingerprint() if (!fingerprint) return false return BUILTIN_PRINTER_DEVICE_KEYWORDS.some(keyword => fingerprint.includes(String(keyword || '').toLowerCase())) } export function getAvailablePrinterTypes (): PrinterType[] { if (isBuiltinPrinterAvailable() && isBuiltinPrinterEnabledByDeviceModel()) { return ['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 { const type = getPrinterType() if (type === 'bluetooth') { const conn = getBluetoothConnection() if (conn && conn.deviceType === 'classic') { 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.')) } 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, serviceId, characteristicId, mtu } = conn const chunks: number[][] = [] for (let i = 0; i < data.length; i += mtu) { chunks.push(data.slice(i, i + mtu)) } const total = chunks.length let sent = 0 function sendNext (): Promise { if (sent >= total) { if (onProgress) onProgress(100) return Promise.resolve() } 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) => { uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId, value: buffer, success: () => { sent++ if (onProgress) onProgress(Math.round((sent / total) * 100)) setTimeout(() => sendNext().then(resolve).catch(reject), 10) }, fail: (err: any) => reject(new Error(err.errMsg || 'BLE write failed')), }) }) } return sendNext() } 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.')) } return new Promise((resolve, reject) => { try { if (!classicBluetooth) { reject(new Error('Classic Bluetooth not available')) return } 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 if (!ready) { const errorMessage = typeof classicBluetooth.getLastError === 'function' ? classicBluetooth.getLastError() : '' reject(buildClassicBluetoothError(errorMessage || 'Classic Bluetooth connection is not ready', conn.deviceId)) return } const sendData = data.map((byte) => { const value = byte & 0xff return value >= 128 ? value - 256 : value }) if (typeof classicBluetooth.sendByteDataAsync === 'function') { classicBluetooth.sendByteDataAsync(sendData, (ok: boolean, errorMessage?: string) => { if (onProgress) onProgress(100) if (ok) { resolve() return } reject(buildClassicBluetoothError( errorMessage || classicBluetooth.getLastError?.() || 'Classic Bluetooth send failed', conn.deviceId )) }) return } const ok = classicBluetooth.sendByteData(sendData) if (onProgress) onProgress(100) if (ok) { resolve() return } const errorMessage = typeof classicBluetooth.getLastError === 'function' ? classicBluetooth.getLastError() : '' reject(buildClassicBluetoothError(errorMessage || 'Classic Bluetooth send failed', conn.deviceId)) } catch (e: any) { 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 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('') 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) moeTcp.connect({ ip: '127.0.0.1', port }, (res: string) => { 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 }) setTimeout(() => { try { moeTcp.disconnect() } catch (_) {} resolve() }, 300) } catch (e) { try { moeTcp.disconnect() } catch (_) {} tryPort(idx + 1).then(resolve).catch(reject) } }) }) } return tryPort(0) } 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 }