From 961eecae3d8a442b5739a022bebd769929b22978 Mon Sep 17 00:00:00 2001 From: “wangming” <“wangming@antissoft.com”> Date: Thu, 19 Mar 2026 15:26:05 +0800 Subject: [PATCH] 对打印机进行开发 --- 美国版/Food Labeling Management App UniApp/src/manifest.json | 13 +++++++++---- 美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue | 341 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue | 21 ++++++--------------- 美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue | 338 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------------------------------------------------------------------------------- 美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothPermissions.ts | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothTool.js | 606 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------- 美国版/Food Labeling Management App UniApp/src/utils/print/drivers/d320fax.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/print/drivers/genericTsc.ts | 38 ++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/print/drivers/gpR3.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/print/manager/driverRegistry.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts | 243 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts | 43 ++++++++++++++++++++++++++++++++----------- 美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/print/protocols/tscProtocol.ts | 10 ++++++++++ 美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 15 files changed, 1529 insertions(+), 494 deletions(-) create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothPermissions.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/print/drivers/d320fax.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/print/drivers/genericTsc.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/print/drivers/gpR3.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/print/manager/driverRegistry.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/print/protocols/tscProtocol.ts create mode 100644 美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts diff --git a/美国版/Food Labeling Management App UniApp/src/manifest.json b/美国版/Food Labeling Management App UniApp/src/manifest.json index ff33eae..4c438b3 100644 --- a/美国版/Food Labeling Management App UniApp/src/manifest.json +++ b/美国版/Food Labeling Management App UniApp/src/manifest.json @@ -2,8 +2,8 @@ "name" : "food.labeling", "appid" : "__UNI__1BFD76D", "description" : "", - "versionName" : "1.0.1", - "versionCode" : 101, + "versionName" : "1.0.3", + "versionCode" : 103, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { @@ -18,8 +18,9 @@ }, /* 模块配置 */ "modules" : { - "Bluetooth" : {}, - "Camera" : {} + "Camera" : {}, + "Barcode" : {}, + "Bluetooth" : {} }, /* 应用发布信息 */ "distribute" : { @@ -43,6 +44,10 @@ "", "", "", + "", + "", + "", + "", "" ] }, diff --git a/美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue b/美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue index 83d0b28..ea39e5d 100644 --- a/美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue +++ b/美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue @@ -42,6 +42,20 @@ {{ errorMsg }} + + Debug Status + Current Mode: {{ debugInfo.currentMode }} + Classic Module: {{ debugInfo.classicModuleReady ? 'Ready' : 'Not Ready' }} + Paired Count: {{ debugInfo.pairedCount }} + Virtual BT Printer: {{ debugInfo.foundVirtualPrinter ? 'Found' : 'Not Found' }} + Classic Scan: {{ debugInfo.lastClassicEvent }} + BLE Scan: {{ debugInfo.lastBleEvent }} + {{ debugInfo.lastBleError }} + + Android system Location service is OFF. Turn it on before BLE scan. + + + @@ -153,26 +167,21 @@ import AppIcon from '../../components/AppIcon.vue' import SideMenu from '../../components/SideMenu.vue' import LocationPicker from '../../components/LocationPicker.vue' import { getStatusBarHeight } from '../../utils/statusBar' +import classicBluetooth from '../../utils/print/bluetoothTool.js' import { getPrinterType, - getBluetoothConnection, - setBluetoothConnection, - setBuiltinPrinter, clearPrinter, - sendToPrinter, type PrinterType, } from '../../utils/print/printerConnection' -import { buildTestTscLabel } from '../../utils/print/tscLabelBuilder' - -function getClassicBluetooth () { - try { - // @ts-ignore - dynamic require for native plugin - const m = require('../../utils/print/bluetoothTool.js') - return m?.default ?? null - } catch { - return null - } -} +import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions' +import { + connectBluetoothPrinter, + describeDiscoveredPrinter, + disconnectCurrentPrinter, + getCurrentPrinterSummary, + testPrintCurrentPrinter, + useBuiltinPrinter, +} from '../../utils/print/manager/printerManager' const statusBarHeight = getStatusBarHeight() const isMenuOpen = ref(false) @@ -181,6 +190,17 @@ const connectingId = ref('') const errorMsg = ref('') const btAdapterReady = ref(false) const printerType = ref(getPrinterType() || 'bluetooth') +const currentPrinter = ref(getCurrentPrinterSummary()) +const debugInfo = ref({ + currentMode: 'none', + classicModuleReady: false, + pairedCount: 0, + foundVirtualPrinter: false, + lastClassicEvent: 'idle', + lastBleEvent: 'idle', + lastBleError: '', + locationServiceRequired: false, +}) interface BtDevice { deviceId: string @@ -192,14 +212,26 @@ interface BtDevice { const devices = ref([]) const discoveredIds = new Set() +function refreshCurrentPrinter () { + currentPrinter.value = getCurrentPrinterSummary() + debugInfo.value.currentMode = currentPrinter.value.type || 'none' +} + +function hasPreferredClassicDeviceInList () { + return devices.value.some((item: any) => { + const name = String(item?.name || '').toLowerCase() + const type = String(item?.type || '').toLowerCase() + return name.includes('virtual bt printer') || type === 'classic' || type === 'dual' + }) +} + const connectedDevice = computed(() => { - const type = getPrinterType() - if (printerType.value === 'builtin' && type === 'builtin') { - return { name: 'Built-in Printer', deviceId: 'builtin' } + const summary = currentPrinter.value + if (printerType.value === 'builtin' && summary.type === 'builtin') { + return { name: summary.displayName, deviceId: summary.deviceId } } - if (printerType.value === 'bluetooth' && type === 'bluetooth') { - const conn = getBluetoothConnection() - if (conn) return { name: conn.deviceName, deviceId: conn.deviceId } + if (printerType.value === 'bluetooth' && summary.type === 'bluetooth') { + return { name: summary.displayName, deviceId: summary.deviceId } } return null }) @@ -208,6 +240,7 @@ function switchType (type: 'bluetooth' | 'builtin') { printerType.value = type if (type === 'bluetooth' && getPrinterType() === 'builtin') { clearPrinter() + refreshCurrentPrinter() } } @@ -240,9 +273,16 @@ const startBleScan = () => { success: () => { isScanning.value = true errorMsg.value = '' + debugInfo.value.lastBleEvent = 'scan running' }, fail: (err: any) => { console.error('BLE startBluetoothDevicesDiscovery fail:', err) + debugInfo.value.lastBleEvent = 'scan failed' + debugInfo.value.lastBleError = err?.errMsg || 'BLE scan failed' + if (err?.errCode === 10016 || err?.code === 10016) { + debugInfo.value.locationServiceRequired = true + errorMsg.value = 'Bluetooth scan failed: Android system Location service is turned off.' + } }, }) } @@ -254,7 +294,7 @@ const stopDiscovery = () => { }, }) // #ifdef APP-PLUS - const classic = getClassicBluetooth() + const classic = classicBluetooth if (classic && classic.cancelClassicDiscovery) classic.cancelClassicDiscovery() // #endif } @@ -266,12 +306,12 @@ const onDeviceFound = (res: any) => { const name = (d.localName || d.name || '').trim() const displayName = name || 'Unknown Device' discoveredIds.add(d.deviceId) - devices.value.push({ + devices.value.push(describeDiscoveredPrinter({ deviceId: d.deviceId, name: displayName, RSSI: d.RSSI, type: 'ble', - }) + })) } devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) } @@ -285,12 +325,12 @@ function mergeCachedBleDevices () { if (discoveredIds.has(d.deviceId)) continue const name = (d.localName || d.name || '').trim() discoveredIds.add(d.deviceId) - devices.value.push({ + devices.value.push(describeDiscoveredPrinter({ deviceId: d.deviceId, name: name || 'Unknown Device', RSSI: d.RSSI, type: 'ble', - }) + })) } if (list.length > 0) devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) }, @@ -299,104 +339,98 @@ function mergeCachedBleDevices () { function addPairedDevices () { // #ifdef APP-PLUS - const classic = getClassicBluetooth() + const classic = classicBluetooth if (!classic || !classic.getPairedDevices) return try { const paired = classic.getPairedDevices() + debugInfo.value.pairedCount = (paired || []).length + debugInfo.value.foundVirtualPrinter = (paired || []).some((item: any) => String(item?.name || '').toLowerCase().includes('virtual bt printer')) for (const p of paired) { if (discoveredIds.has(p.deviceId)) continue discoveredIds.add(p.deviceId) - devices.value.push({ + devices.value.push(describeDiscoveredPrinter({ deviceId: p.deviceId, - name: p.name || 'Unknown', + name: p.name || 'Unknown Device', type: p.type || 'classic', - }) + })) } if (paired.length > 0) { + debugInfo.value.lastClassicEvent = 'paired devices loaded' devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) + } else { + debugInfo.value.lastClassicEvent = 'no paired devices' } } catch (e) { console.error('addPairedDevices error:', e) + debugInfo.value.lastClassicEvent = 'load paired devices failed' } // #endif } -function findWriteCharacteristic (deviceId: string): Promise<{ serviceId: string; characteristicId: string } | null> { - return new Promise((resolve) => { - uni.getBLEDeviceServices({ - deviceId, - success: (sres) => { - const services = sres.services || [] - const tryNext = (idx: number) => { - if (idx >= services.length) { - resolve(null) - return - } - const serviceId = services[idx].uuid - uni.getBLEDeviceCharacteristics({ - deviceId, - serviceId, - success: (cres) => { - const chars = cres.characteristics || [] - const writeChar = chars.find((c: any) => c.properties && c.properties.write) - if (writeChar) { - resolve({ serviceId, characteristicId: writeChar.uuid }) - return - } - tryNext(idx + 1) - }, - fail: () => tryNext(idx + 1), - }) - } - tryNext(0) - }, - fail: () => resolve(null), - }) - }) -} - -const handleScan = async () => { - if (isScanning.value) { - stopDiscovery() - return - } - errorMsg.value = '' - devices.value = [] - discoveredIds.clear() - - addPairedDevices() - +function startClassicScan () { // #ifdef APP-PLUS - const classic = getClassicBluetooth() + const classic = classicBluetooth if (classic && classic.startClassicDiscovery) { try { classic.startClassicDiscovery( (dev: { name: string; deviceId: string; type: string }) => { + debugInfo.value.lastClassicEvent = 'device found' if (discoveredIds.has(dev.deviceId)) return discoveredIds.add(dev.deviceId) - devices.value.push({ + devices.value.push(describeDiscoveredPrinter({ deviceId: dev.deviceId, - name: dev.name || 'Unknown', + name: dev.name || 'Unknown Device', type: (dev.type as BtDevice['type']) || 'classic', - }) + })) + devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) + }, + () => { + debugInfo.value.lastClassicEvent = 'scan finished' }, - () => {}, ) isScanning.value = true + debugInfo.value.lastClassicEvent = 'scan running' } catch (e) { console.error('Classic discovery failed', e) + debugInfo.value.lastClassicEvent = 'scan failed' } + } else { + debugInfo.value.lastClassicEvent = 'classic module unavailable' } // #endif +} - if (devices.value.length > 0) { - uni.showToast({ title: `Found ${devices.value.length} device(s)`, icon: 'none' }) +const handleScan = async () => { + if (isScanning.value) { + stopDiscovery() + return } + errorMsg.value = '' + devices.value = [] + discoveredIds.clear() + debugInfo.value.lastBleError = '' + debugInfo.value.locationServiceRequired = false + debugInfo.value.lastClassicEvent = 'starting' + debugInfo.value.lastBleEvent = 'starting' try { - if (!btAdapterReady.value) { - await initBluetooth() + const permissionResult = await ensureBluetoothPermissions({ scan: true, connect: true }) + if (!permissionResult.ok) { + errorMsg.value = permissionResult.message || 'Bluetooth permission denied.' + return } + await initBluetooth() + + addPairedDevices() + startClassicScan() + + if (hasPreferredClassicDeviceInList()) { + isScanning.value = false + debugInfo.value.lastBleEvent = 'skipped (paired classic device found)' + uni.showToast({ title: 'Using paired classic Bluetooth devices', icon: 'none' }) + return + } + mergeCachedBleDevices() await new Promise((resolve) => { uni.getLocation({ @@ -424,77 +458,27 @@ const handleConnect = async (dev: BtDevice) => { if (isScanning.value) stopDiscovery() - // 经典蓝牙:classic、dual、unknown(部分设备如 D320FAX 可能误报为 unknown,也尝试经典连接) - const useClassic = dev.type === 'classic' || dev.type === 'dual' || dev.type === 'unknown' - - if (useClassic) { - // #ifdef APP-PLUS - const classic = getClassicBluetooth() - if (classic && classic.connDevice) { - classic.connDevice(dev.deviceId, (ok: boolean) => { - connectingId.value = '' - if (ok) { - setBluetoothConnection({ - deviceId: dev.deviceId, - deviceName: dev.name, - deviceType: 'classic', - }) - uni.showToast({ title: 'Connected!', icon: 'success' }) - } else { - errorMsg.value = 'Connection failed. For D320FAX, ensure Virtual BT Printer is paired in system Bluetooth.' - } - }) - } else { - connectingId.value = '' - errorMsg.value = 'Classic Bluetooth not available. Ensure app is running on the device (not simulator).' - } - // #endif - // #ifndef APP-PLUS + const permissionResult = await ensureBluetoothPermissions({ connect: true }) + if (!permissionResult.ok) { connectingId.value = '' - errorMsg.value = 'Classic Bluetooth requires the app.' - // #endif + errorMsg.value = permissionResult.message || 'Bluetooth permission denied.' return } - uni.createBLEConnection({ - deviceId: dev.deviceId, - timeout: 10000, - success: async () => { - try { - const write = await findWriteCharacteristic(dev.deviceId) - if (!write) { - errorMsg.value = 'No writable characteristic found. This device may not support printing.' - connectingId.value = '' - return - } - setBluetoothConnection({ - deviceId: dev.deviceId, - deviceName: dev.name, - serviceId: write.serviceId, - characteristicId: write.characteristicId, - deviceType: 'ble', - mtu: 20, - }) - connectingId.value = '' - uni.showToast({ title: 'Connected!', icon: 'success' }) - } catch (e: any) { - errorMsg.value = (e && e.message) ? e.message : 'Connection failed' - connectingId.value = '' - } - }, - fail: (err: any) => { - connectingId.value = '' - if (err.errCode === -1) { - uni.showToast({ title: 'Already connected', icon: 'success' }) - } else { - errorMsg.value = 'Connection failed: ' + (err.errMsg || 'Try again') - } - }, - }) + try { + await connectBluetoothPrinter(dev) + refreshCurrentPrinter() + connectingId.value = '' + uni.showToast({ title: 'Connected!', icon: 'success' }) + } catch (e: any) { + errorMsg.value = (e && e.message) ? e.message : 'Connection failed' + connectingId.value = '' + } } const handleUseBuiltin = () => { - setBuiltinPrinter() + useBuiltinPrinter() + refreshCurrentPrinter() uni.showToast({ title: 'Using built-in printer', icon: 'success' }) } @@ -503,8 +487,7 @@ const handleTestPrint = async () => { if (testPrinting.value) return testPrinting.value = true try { - const data = buildTestTscLabel() - await sendToPrinter(data, (p) => { + await testPrintCurrentPrinter((p) => { if (p < 100) uni.showLoading({ title: `Printing ${p}%`, mask: true }) }) uni.hideLoading() @@ -531,30 +514,14 @@ const handleTestPrint = async () => { } } -const handleDisconnect = () => { - const type = getPrinterType() - const deviceId = uni.getStorageSync('btDeviceId') - const deviceType = uni.getStorageSync('btDeviceType') - if (type === 'bluetooth' && deviceType === 'classic') { - // #ifdef APP-PLUS - const classic = getClassicBluetooth() - if (classic && classic.disConnDevice) classic.disConnDevice() - // #endif - } - clearPrinter() - if (type === 'bluetooth' && deviceId && deviceType !== 'classic') { - uni.closeBLEConnection({ - deviceId, - complete: () => { - uni.showToast({ title: 'Disconnected', icon: 'none' }) - }, - }) - } else { - uni.showToast({ title: 'Disconnected', icon: 'none' }) - } +const handleDisconnect = async () => { + await disconnectCurrentPrinter() + refreshCurrentPrinter() + uni.showToast({ title: 'Disconnected', icon: 'none' }) } onMounted(() => { + debugInfo.value.classicModuleReady = !!classicBluetooth uni.onBluetoothDeviceFound(onDeviceFound) uni.onBluetoothAdapterStateChange((res: any) => { if (!res.available) { @@ -567,6 +534,7 @@ onMounted(() => { } }) printerType.value = getPrinterType() || 'bluetooth' + refreshCurrentPrinter() }) onUnmounted(() => { @@ -901,6 +869,37 @@ onUnmounted(() => { gap: 12rpx; } +.debug-card { + background: #fff7ed; + border: 1rpx solid #fed7aa; + border-radius: 20rpx; + padding: 24rpx; + margin-bottom: 24rpx; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.debug-title { + font-size: 28rpx; + font-weight: 700; + color: #9a3412; +} + +.debug-item { + font-size: 22rpx; + color: #7c2d12; +} + +.debug-error { + color: #b91c1c; +} + +.debug-warn { + color: #92400e; + font-weight: 600; +} + .device-tag { display: inline-block; font-size: 20rpx; diff --git a/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue b/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue index d5d7093..f6b9771 100644 --- a/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue +++ b/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue @@ -118,8 +118,7 @@ import SideMenu from '../../components/SideMenu.vue' import LocationPicker from '../../components/LocationPicker.vue' import { getStatusBarHeight } from '../../utils/statusBar' import { generateNextLabelId } from '../../utils/printLog' -import { getPrinterType, getBluetoothConnection, sendToPrinter } from '../../utils/print/printerConnection' -import { buildTscLabel } from '../../utils/print/tscLabelBuilder' +import { getCurrentPrinterSummary, printLabelForCurrentPrinter } from '../../utils/print/manager/printerManager' import chickenLabelImg from '../../static/chicken-lable.png' const statusBarHeight = getStatusBarHeight() @@ -161,16 +160,9 @@ const displayProductName = computed(() => { }) onShow(() => { - const type = getPrinterType() - const conn = type === 'bluetooth' ? getBluetoothConnection() : null - btConnected.value = (type === 'bluetooth' && conn) || type === 'builtin' - if (type === 'builtin') { - btDeviceName.value = 'Built-in Printer' - } else if (conn) { - btDeviceName.value = conn.deviceName || '' - } else { - btDeviceName.value = '' - } + const summary = getCurrentPrinterSummary() + btConnected.value = summary.type === 'bluetooth' || summary.type === 'builtin' + btDeviceName.value = summary.displayName || '' }) interface ProductData { @@ -245,13 +237,12 @@ const handlePrint = async () => { } isPrinting.value = true try { - const data = buildTscLabel({ + await printLabelForCurrentPrinter({ productName: displayProductName.value, labelId: labelId.value, printQty: printQty.value, extraLine: lastEdited.value, - }) - await sendToPrinter(data, (percent) => { + }, (percent) => { if (percent >= 100) return uni.showLoading({ title: `Printing ${percent}%`, mask: true }) }) diff --git a/美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue b/美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue index 15f1a4f..c8aa460 100644 --- a/美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue +++ b/美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue @@ -21,6 +21,20 @@ {{ t('printers.connected') }} + + Debug Status + Current Mode: {{ debugInfo.currentMode }} + Classic Module: {{ debugInfo.classicModuleReady ? 'Ready' : 'Not Ready' }} + Paired Count: {{ debugInfo.pairedCount }} + Virtual BT Printer: {{ debugInfo.foundVirtualPrinter ? 'Found' : 'Not Found' }} + Classic Scan: {{ debugInfo.lastClassicEvent }} + BLE Scan: {{ debugInfo.lastBleEvent }} + {{ debugInfo.lastBleError }} + + Android system Location service is OFF. Turn it on in device settings before BLE scan. + + + @@ -105,35 +119,51 @@ import { ref, onMounted, onUnmounted } from 'vue' import { useI18n } from 'vue-i18n' import AppIcon from '../../components/AppIcon.vue' import SideMenu from '../../components/SideMenu.vue' +import classicBluetooth from '../../utils/print/bluetoothTool.js' +import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions' import { - getPrinterType, - getBluetoothConnection, - setBluetoothConnection, - setBuiltinPrinter, - clearPrinter, - sendToPrinter, - isBuiltinConnected -} from '../../utils/print/printerConnection' -import { buildTestTscLabel } from '../../utils/print/tscLabelBuilder' - -// #ifdef APP-PLUS -const classicBluetooth = (require('../../utils/print/bluetoothTool.js') as any).default -// #endif + connectBluetoothPrinter, + describeDiscoveredPrinter, + disconnectCurrentPrinter, + getCurrentPrinterSummary, + testPrintCurrentPrinter, + useBuiltinPrinter, +} from '../../utils/print/manager/printerManager' const { t } = useI18n() const isMenuOpen = ref(false) -const currentType = ref(getPrinterType()) -const currentBt = ref(getBluetoothConnection()) +const currentType = ref<'' | 'bluetooth' | 'builtin'>('') +const currentBt = ref(null) const isScanning = ref(false) const devices = ref([]) const pairedDevices = ref([]) const isConnecting = ref(false) +const debugInfo = ref({ + currentMode: 'none', + classicModuleReady: false, + pairedCount: 0, + foundVirtualPrinter: false, + lastClassicEvent: 'idle', + lastBleEvent: 'idle', + lastBleError: '', + locationServiceRequired: false, +}) let bleListenerRegistered = false const refreshStatus = () => { - currentType.value = getPrinterType() - currentBt.value = getBluetoothConnection() + const summary = getCurrentPrinterSummary() + currentType.value = summary.type + debugInfo.value.currentMode = summary.type || 'none' + currentBt.value = summary.type === 'bluetooth' + ? { + deviceName: summary.displayName, + deviceId: summary.deviceId, + deviceType: summary.deviceType, + driverName: summary.driverName, + protocol: summary.protocol, + } + : null } const getTypeLabel = (type?: string) => { @@ -145,64 +175,140 @@ const getTypeLabel = (type?: string) => { } } +const normalizeDeviceName = (device: any) => { + return (device?.localName || device?.name || '').trim() || 'Unknown Device' +} + +const hasPreferredClassicDevice = () => { + return pairedDevices.value.some((item: any) => { + const name = String(item?.name || '').toLowerCase() + const type = String(item?.type || '').toLowerCase() + return name.includes('virtual bt printer') || type === 'classic' || type === 'dual' + }) +} + const addDeviceDedup = (device: any) => { + const described = describeDiscoveredPrinter(device) const existing = devices.value.find(d => d.deviceId === device.deviceId) if (!existing) { - devices.value.push(device) + devices.value.push(described) + return } + Object.assign(existing, described) } // #ifdef APP-PLUS const loadPairedDevices = () => { try { const list = classicBluetooth.getPairedDevices() - pairedDevices.value = list || [] + debugInfo.value.pairedCount = (list || []).length + debugInfo.value.foundVirtualPrinter = (list || []).some((item: any) => String(item?.name || '').toLowerCase().includes('virtual bt printer')) + pairedDevices.value = (list || []).map((item: any) => ({ + ...describeDiscoveredPrinter(item), + name: normalizeDeviceName(item), + })) + debugInfo.value.lastClassicEvent = pairedDevices.value.length > 0 ? 'paired devices loaded' : 'no paired devices' } catch (e) { console.error('Failed to load paired devices', e) pairedDevices.value = [] + debugInfo.value.pairedCount = 0 + debugInfo.value.foundVirtualPrinter = false + debugInfo.value.lastClassicEvent = 'load paired devices failed' } } // #endif -const startScan = () => { +function mergeCachedBleDevices () { + uni.getBluetoothDevices({ + success: (res: any) => { + const list = res.devices || [] + list.forEach((device: any) => { + addDeviceDedup({ + ...device, + name: normalizeDeviceName(device), + type: device.type || 'ble', + }) + }) + }, + }) +} + +const startScan = async () => { if (isScanning.value) return devices.value = [] - isScanning.value = true - - // #ifdef APP-PLUS - loadPairedDevices() - try { - classicBluetooth.startClassicDiscovery( - (device: any) => { - if (device.name) { - addDeviceDedup(device) - } - }, - () => {} - ) - } catch (e) { - console.error('Classic discovery failed', e) + debugInfo.value.lastBleError = '' + debugInfo.value.locationServiceRequired = false + debugInfo.value.lastClassicEvent = 'starting' + debugInfo.value.lastBleEvent = 'starting' + + const permissionResult = await ensureBluetoothPermissions({ scan: true, connect: true }) + if (!permissionResult.ok) { + uni.showToast({ title: permissionResult.message || t('printers.bleNotAvailable'), icon: 'none' }) + return } - // #endif uni.openBluetoothAdapter({ success: () => { + // #ifdef APP-PLUS + loadPairedDevices() + try { + classicBluetooth.startClassicDiscovery( + (device: any) => { + debugInfo.value.lastClassicEvent = 'device found' + addDeviceDedup({ + ...device, + name: normalizeDeviceName(device), + }) + }, + () => { + debugInfo.value.lastClassicEvent = 'scan finished' + } + ) + debugInfo.value.lastClassicEvent = 'scan running' + } catch (e) { + console.error('Classic discovery failed', e) + debugInfo.value.lastClassicEvent = 'scan failed' + } + // #endif + isScanning.value = true + + if (hasPreferredClassicDevice()) { + debugInfo.value.lastBleEvent = 'skipped (paired classic device found)' + return + } + + mergeCachedBleDevices() if (!bleListenerRegistered) { bleListenerRegistered = true uni.onBluetoothDeviceFound((res) => { res.devices.forEach(device => { - if (device.name) { - addDeviceDedup({ ...device, type: 'ble' }) - } + addDeviceDedup({ + ...device, + name: normalizeDeviceName(device), + type: 'ble', + }) }) }) } uni.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, - success: () => {}, + success: () => { + debugInfo.value.lastBleEvent = 'scan running' + }, fail: (err) => { console.error('BLE startBluetoothDevicesDiscovery fail:', err) + debugInfo.value.lastBleEvent = 'scan failed' + debugInfo.value.lastBleError = err?.errMsg || 'BLE scan failed' + if (err?.errCode === 10016 || err?.code === 10016) { + debugInfo.value.locationServiceRequired = true + debugInfo.value.lastBleError = 'BLE scan failed: system Location service is turned off.' + uni.showToast({ + title: 'Turn on system Location service first', + icon: 'none', + duration: 2500, + }) + } } }) }, @@ -217,6 +323,8 @@ const startScan = () => { const stopScan = () => { isScanning.value = false + debugInfo.value.lastClassicEvent = 'stopped' + debugInfo.value.lastBleEvent = 'stopped' uni.stopBluetoothDevicesDiscovery({ success: () => console.log('Stop BLE scan success'), fail: (err) => console.error('Stop BLE scan fail', err) @@ -230,140 +338,56 @@ const stopScan = () => { // #endif } -const connectBt = (device: any) => { +const connectBt = async (device: any) => { if (isConnecting.value) return isConnecting.value = true stopScan() - uni.showLoading({ title: t('printers.connecting') }) - - const deviceType = device.type || 'ble' - - if (deviceType === 'classic' || deviceType === 'dual') { - // #ifdef APP-PLUS - classicBluetooth.connDevice(device.deviceId, (success: boolean) => { - isConnecting.value = false - uni.hideLoading() - if (success) { - setBluetoothConnection({ - deviceId: device.deviceId, - deviceName: device.name || 'Bluetooth Printer', - deviceType: 'classic' - }) - currentType.value = 'bluetooth' - currentBt.value = getBluetoothConnection() - uni.showToast({ title: t('printers.connectSuccess') }) - } else { - uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) - } - }) - // #endif - // #ifndef APP-PLUS + const permissionResult = await ensureBluetoothPermissions({ connect: true }) + if (!permissionResult.ok) { isConnecting.value = false - uni.hideLoading() - uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) - // #endif - } else { - connectBle(device) + uni.showToast({ title: permissionResult.message || t('printers.connectFail'), icon: 'none' }) + return } -} - -const connectBle = (device: any) => { - uni.createBLEConnection({ - deviceId: device.deviceId, - success: () => { - uni.getBLEDeviceServices({ - deviceId: device.deviceId, - success: (res) => { - findWriteCharacteristic(device.deviceId, res.services, device.name) - }, - fail: () => { - isConnecting.value = false - uni.hideLoading() - uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) - } - }) - }, - fail: () => { - isConnecting.value = false - uni.hideLoading() - uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) - } - }) -} -const findWriteCharacteristic = (deviceId: string, services: any[], deviceName: string) => { - let found = false - let serviceIdx = 0 - - const nextService = () => { - if (serviceIdx >= services.length || found) { - if (!found) { - isConnecting.value = false - uni.hideLoading() - uni.showToast({ title: 'No write characteristic found', icon: 'none' }) - } - return - } - - const service = services[serviceIdx++] - uni.getBLEDeviceCharacteristics({ - deviceId, - serviceId: service.uuid, - success: (res) => { - const char = res.characteristics.find(c => c.properties.write) - if (char) { - found = true - setBluetoothConnection({ - deviceId, - deviceName, - serviceId: service.uuid, - characteristicId: char.uuid, - deviceType: 'ble' - }) - currentType.value = 'bluetooth' - currentBt.value = getBluetoothConnection() - isConnecting.value = false - uni.hideLoading() - uni.showToast({ title: t('printers.connectSuccess') }) - } else { - nextService() - } - }, - fail: () => nextService() - }) + uni.showLoading({ title: t('printers.connecting') }) + try { + await connectBluetoothPrinter(device) + refreshStatus() + uni.hideLoading() + uni.showToast({ title: t('printers.connectSuccess') }) + } catch (e) { + uni.hideLoading() + uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) + } finally { + isConnecting.value = false } - - nextService() } const connectBuiltin = () => { - setBuiltinPrinter() - currentType.value = 'builtin' + useBuiltinPrinter() + refreshStatus() uni.showToast({ title: t('printers.connectSuccess') }) } -const disconnect = () => { - // #ifdef APP-PLUS - if (currentBt.value?.deviceType === 'classic') { - try { - classicBluetooth.disConnDevice() - } catch (e) { - console.error('Disconnect classic bluetooth failed', e) - } - } - // #endif - clearPrinter() - currentType.value = '' - currentBt.value = null +const disconnect = async () => { + await disconnectCurrentPrinter() + refreshStatus() uni.showToast({ title: t('printers.disconnected') }) } const doTestPrint = async () => { try { - const data = buildTestTscLabel() + if (currentType.value === 'builtin') { + uni.showModal({ + title: 'Current Mode is Built-in', + content: 'This handheld device is currently using Built-in mode. Please disconnect it, then connect "Virtual BT Printer" from the Paired Devices list and test print again.', + showCancel: false, + }) + return + } uni.showLoading({ title: t('labels.print.printing') }) - await sendToPrinter(data) + await testPrintCurrentPrinter() uni.hideLoading() uni.showToast({ title: t('printers.testPrintSuccess') }) } catch (e: any) { @@ -393,6 +417,7 @@ const goBack = () => { onMounted(() => { refreshStatus() + debugInfo.value.classicModuleReady = !!classicBluetooth // #ifdef APP-PLUS loadPairedDevices() // #endif @@ -428,6 +453,11 @@ onUnmounted(() => { .info-badge { display: flex; align-items: center; gap: 12rpx; margin-bottom: 32rpx; } .badge-num { font-size: 40rpx; font-weight: 700; color: var(--theme-primary); } .badge-text { font-size: 28rpx; color: #6b7280; } +.debug-card { background: #fff7ed; border: 1rpx solid #fed7aa; border-radius: 20rpx; padding: 24rpx; margin-bottom: 24rpx; display: flex; flex-direction: column; gap: 8rpx; } +.debug-title { font-size: 28rpx; font-weight: 700; color: #9a3412; } +.debug-item { font-size: 22rpx; color: #7c2d12; } +.debug-error { color: #b91c1c; } +.debug-warn { color: #92400e; font-weight: 600; } .printer-card { background: #fff; padding: 32rpx; border-radius: 24rpx; margin-bottom: 24rpx; display: flex; align-items: center; gap: 24rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); border: 1rpx solid #f3f4f6; } .printer-card.connected { background: var(--theme-primary); border: none; } .printer-card.connected .printer-name, .printer-card.connected .printer-loc, .printer-card.connected .printer-status { color: #fff; } diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothPermissions.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothPermissions.ts new file mode 100644 index 0000000..19dacac --- /dev/null +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothPermissions.ts @@ -0,0 +1,92 @@ +export interface BluetoothPermissionResult { + ok: boolean + message?: string +} + +function normalizePermissionName (permission: string): string { + return String(permission || '').split('.').pop() || String(permission || '') +} + +function formatDeniedMessage (permissions: string[]): string { + if (!permissions.length) return 'Bluetooth permission denied.' + return 'Please allow Bluetooth permissions: ' + permissions.map(normalizePermissionName).join(', ') +} + +function requestAndroidPermissions (permissions: string[]): Promise { + return new Promise((resolve) => { + // #ifdef APP-PLUS + try { + if (typeof plus === 'undefined' || !plus.android || !permissions.length) { + resolve({ ok: true }) + return + } + plus.android.requestPermissions( + permissions, + (resultObj: any) => { + const deniedPresent = Array.isArray(resultObj?.deniedPresent) ? resultObj.deniedPresent : [] + const deniedAlways = Array.isArray(resultObj?.deniedAlways) ? resultObj.deniedAlways : [] + const denied = [...deniedPresent, ...deniedAlways] + if (denied.length > 0) { + resolve({ + ok: false, + message: formatDeniedMessage(denied), + }) + return + } + resolve({ ok: true }) + }, + () => { + resolve({ + ok: false, + message: 'Failed to request Bluetooth permissions.', + }) + } + ) + return + } catch (e: any) { + resolve({ + ok: false, + message: e?.message || 'Failed to request Bluetooth permissions.', + }) + return + } + // #endif + resolve({ ok: true }) + }) +} + +export async function ensureBluetoothPermissions (options?: { + scan?: boolean + connect?: boolean +}): Promise { + const { scan = false, connect = false } = options || {} + + // #ifdef APP-PLUS + try { + if (typeof plus === 'undefined' || !plus.android) { + return { ok: true } + } + const Build = plus.android.importClass('android.os.Build') + const sdkInt = Number(Build.VERSION.SDK_INT || 0) + + const permissions = new Set() + if (sdkInt >= 31) { + if (scan) permissions.add('android.permission.BLUETOOTH_SCAN') + if (scan || connect) permissions.add('android.permission.BLUETOOTH_CONNECT') + if (scan) permissions.add('android.permission.ACCESS_FINE_LOCATION') + } else if (scan) { + permissions.add('android.permission.ACCESS_FINE_LOCATION') + permissions.add('android.permission.ACCESS_COARSE_LOCATION') + } + + return await requestAndroidPermissions(Array.from(permissions)) + } catch (e: any) { + return { + ok: false, + message: e?.message || 'Bluetooth permission check failed.', + } + } + // #endif + + return { ok: true } +} diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothTool.js b/美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothTool.js index bbc67c1..6f0667e 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothTool.js +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothTool.js @@ -1,83 +1,207 @@ /** - * 经典蓝牙工具(仅 Android APP-PLUS) - * 用于佳博 D320FAX 等一体机的 Virtual BT Printer 连接 - * 支持 RFCOMM 回退连接(部分设备需要) + * 经典蓝牙工具(Android APP-PLUS) + * 兼容官方 UniApp SDK 经典蓝牙实现,并补充当前项目需要的简化调用: + * - getPairedDevices() + * - startClassicDiscovery(onDeviceFound, onDiscoveryFinished) + * - cancelClassicDiscovery() + * - connDevice(address, callback) + * - disConnDevice() + * - sendByteData(byteData) */ + // #ifdef APP-PLUS -function getAdapter () { - if (typeof plus === 'undefined' || !plus.android) return null - const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter') - return BluetoothAdapter.getDefaultAdapter() -} +let BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter') +let Intent = plus.android.importClass('android.content.Intent') +let IntentFilter = plus.android.importClass('android.content.IntentFilter') +let BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice') +let UUID = plus.android.importClass('java.util.UUID') +let Toast = plus.android.importClass('android.widget.Toast') +let MY_UUID = UUID.fromString('00001101-0000-1000-8000-00805F9B34FB') + +let invoke = plus.android.invoke +let btAdapter = BluetoothAdapter.getDefaultAdapter() +let activity = plus.android.runtimeMainActivity() + +let btSocket = null +let btInStream = null +let btOutStream = null +let setIntervalId = 0 + +let btFindReceiver = null +let btStatusReceiver = null +// #endif -function getActivity () { - return typeof plus !== 'undefined' ? plus.android.runtimeMainActivity() : null +function normalizeDeviceType (deviceType) { + if (deviceType === 1) return 'classic' + if (deviceType === 2) return 'ble' + if (deviceType === 3) return 'dual' + return 'unknown' } -function showToast (msg) { +function fallbackRfcommSocket (device, insecure = false) { try { - const activity = getActivity() - if (activity) { - const Toast = plus.android.importClass('android.widget.Toast') - Toast.makeText(activity, String(msg), Toast.LENGTH_SHORT).show() - } - } catch (_) {} + const cls = invoke(device, 'getClass') + const intClass = plus.android.importClass('java.lang.Integer').TYPE + const methodName = insecure ? 'createInsecureRfcommSocket' : 'createRfcommSocket' + const method = invoke(cls, 'getMethod', methodName, intClass) + return invoke(method, 'invoke', device, 1) + } catch (e) { + console.error('RFCOMM fallback failed:', e) + return null + } } -function getInvoke () { - return typeof plus !== 'undefined' && plus.android ? plus.android.invoke : null +function getErrorMessage (error) { + if (!error) return 'Unknown error' + if (typeof error === 'string') return error + return String(error.message || error.errMsg || error) } -function getMyUuid () { - if (typeof plus === 'undefined' || !plus.android) return null - return plus.android.importClass('java.util.UUID').fromString('00001101-0000-1000-8000-00805F9B34FB') + +function normalizeWriteByte (value) { + const byte = Number(value) || 0 + return byte & 0xff +} + +function normalizeWriteChunk (byteData, start, end) { + const out = [] + for (let i = start; i < end; i++) { + const value = normalizeWriteByte(byteData[i]) + if (value >= 128) { + out.push(value - 256) + } else { + out.push(value) + } + } + return out +} + +function createSocketCandidates (device) { + return [ + { + name: 'secure-service-record', + create: () => invoke(device, 'createRfcommSocketToServiceRecord', MY_UUID), + }, + { + name: 'insecure-service-record', + create: () => invoke(device, 'createInsecureRfcommSocketToServiceRecord', MY_UUID), + }, + { + name: 'secure-channel-1', + create: () => fallbackRfcommSocket(device, false), + }, + { + name: 'insecure-channel-1', + create: () => fallbackRfcommSocket(device, true), + }, + ] } -let btSocket = null -let btInStream = null -let btOutStream = null -let btFindReceiver = null -// #endif var blueToothTool = { - state: { bluetoothEnable: false, readThreadState: false }, + state: { + bluetoothEnable: false, + bluetoothState: '', + discoveryDeviceState: false, + readThreadState: false, + connectionState: 'idle', + lastAddress: '', + lastSocketStrategy: '', + lastError: '', + lastSendError: '', + outputReady: false, + lastSendMode: 'idle', + }, + options: { + listenBTStatusCallback: function (state) {}, + discoveryDeviceCallback: function (newDevice) {}, + discoveryFinishedCallback: function () {}, + readDataCallback: function (dataByteArr) {}, + connExceptionCallback: function (e) {}, + }, + init (setOptions) { + Object.assign(this.options, setOptions || {}) + this.state.bluetoothEnable = this.getBluetoothStatus() + this.listenBluetoothStatus() + }, + setErrorState (message, type = 'general') { + const text = String(message || '') + this.state.lastError = text + if (type === 'send') { + this.state.lastSendError = text + } + }, + clearErrorState () { + this.state.lastError = '' + this.state.lastSendError = '' + }, shortToast (msg) { // #ifdef APP-PLUS - showToast(msg) + try { + if (activity) Toast.makeText(activity, String(msg), Toast.LENGTH_SHORT).show() + } catch (_) {} // #endif }, + isSupportBluetooth () { + // #ifdef APP-PLUS + return btAdapter != null + // #endif + return false + }, getBluetoothStatus () { // #ifdef APP-PLUS - const btAdapter = getAdapter() return btAdapter != null && btAdapter.isEnabled() // #endif return false }, - /** 获取已配对设备(含 Virtual BT Printer / D320FAX) */ + turnOnBluetooth () { + // #ifdef APP-PLUS + if (btAdapter == null) { + this.shortToast('Bluetooth not available') + return + } + if (!btAdapter.isEnabled()) { + if (activity == null) { + this.shortToast('Activity not available') + return + } + let intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + activity.startActivityForResult(intent, 1) + return + } + this.shortToast('Bluetooth already enabled') + // #endif + }, + turnOffBluetooth () { + // #ifdef APP-PLUS + if (btAdapter != null && btAdapter.isEnabled()) { + btAdapter.disable() + } + if (btFindReceiver != null) { + try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} + btFindReceiver = null + } + this.state.bluetoothEnable = false + this.cancelDiscovery() + this.closeBtSocket() + // #endif + }, getPairedDevices () { // #ifdef APP-PLUS - const pairedDevices = [] + let pairedDevices = [] try { - const btAdapter = getAdapter() - if (!btAdapter || !btAdapter.isEnabled()) { - this.shortToast('Bluetooth is off') - return pairedDevices - } - const bonded = btAdapter.getBondedDevices() - if (!bonded) return pairedDevices - const inv = getInvoke() - if (!inv) return pairedDevices - const it = inv(bonded, 'iterator') - while (inv(it, 'hasNext')) { - const device = inv(it, 'next') - const deviceType = inv(device, 'getType') - const deviceId = inv(device, 'getAddress') - let deviceName = inv(device, 'getName') - if (deviceName == null) deviceName = '' - deviceName = String(deviceName).trim() || 'Unknown Device' - let typeStr = 'unknown' - if (deviceType === 1) typeStr = 'classic' - else if (deviceType === 2) typeStr = 'ble' - else if (deviceType === 3) typeStr = 'dual' - pairedDevices.push({ name: deviceName, deviceId: String(deviceId), type: typeStr }) + if (btAdapter == null || !btAdapter.isEnabled()) return pairedDevices + let pairedDevicesAndroid = btAdapter.getBondedDevices() + if (!pairedDevicesAndroid) return pairedDevices + let it = invoke(pairedDevicesAndroid, 'iterator') + while (invoke(it, 'hasNext')) { + let device = invoke(it, 'next') + let deviceType = invoke(device, 'getType') + let deviceId = invoke(device, 'getAddress') + let deviceName = invoke(device, 'getName') + pairedDevices.push({ + name: deviceName != null ? String(deviceName).trim() || 'Unknown Device' : 'Unknown Device', + deviceId: String(deviceId || ''), + type: normalizeDeviceType(deviceType), + }) } } catch (e) { console.error('getPairedDevices error:', e) @@ -86,156 +210,360 @@ var blueToothTool = { // #endif return [] }, - /** 经典蓝牙扫描(发现未配对设备,如 d320fax_295c)— 不过滤任何设备 */ - startClassicDiscovery (onDeviceFound, onDiscoveryFinished) { + discoveryNewDevice () { // #ifdef APP-PLUS - const btAdapter = getAdapter() - const activity = getActivity() - if (!btAdapter || !btAdapter.isEnabled() || !activity) return - if (btFindReceiver) { - try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} + if (btAdapter == null || !btAdapter.isEnabled() || activity == null) return + if (btFindReceiver != null) { + try { activity.unregisterReceiver(btFindReceiver) } catch (e) { console.error(e) } btFindReceiver = null + this.cancelDiscovery() } - if (btAdapter.isDiscovering()) btAdapter.cancelDiscovery() - const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter') - const BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice') - const IntentFilter = plus.android.importClass('android.content.IntentFilter') - const inv = getInvoke() + let options = this.options btFindReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', { - onReceive (context, intent) { - const action = intent.getAction() + onReceive: function (context, intent) { + plus.android.importClass(context) + plus.android.importClass(intent) + let action = intent.getAction() if (BluetoothDevice.ACTION_FOUND === action) { - const device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + let device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) if (!device) return - const deviceType = inv(device, 'getType') - const deviceId = String(inv(device, 'getAddress') || '') - let deviceName = inv(device, 'getName') - if (deviceName == null) deviceName = '' - deviceName = String(deviceName).trim() || 'Unknown Device' - let typeStr = 'unknown' - if (deviceType === 1) typeStr = 'classic' - else if (deviceType === 2) typeStr = 'ble' - else if (deviceType === 3) typeStr = 'dual' - if (onDeviceFound) onDeviceFound({ name: deviceName, deviceId, type: typeStr }) + let deviceType = invoke(device, 'getType') + let deviceId = invoke(device, 'getAddress') + let deviceName = invoke(device, 'getName') + let newDevice = { + name: deviceName != null ? String(deviceName).trim() || 'Unknown Device' : 'Unknown Device', + deviceId: String(deviceId || ''), + type: normalizeDeviceType(deviceType), + } + options.discoveryDeviceCallback && options.discoveryDeviceCallback(newDevice) } if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED === action) { - try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} - btFindReceiver = null - if (onDiscoveryFinished) onDiscoveryFinished() + blueToothTool.cancelDiscovery() + options.discoveryFinishedCallback && options.discoveryFinishedCallback() } }, }) - const filter = new IntentFilter() + let filter = new IntentFilter() filter.addAction(BluetoothDevice.ACTION_FOUND) filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) activity.registerReceiver(btFindReceiver, filter) btAdapter.startDiscovery() + this.state.discoveryDeviceState = true // #endif }, - /** 停止经典蓝牙扫描 */ - cancelClassicDiscovery () { + startClassicDiscovery (onDeviceFound, onDiscoveryFinished) { + this.init({ + discoveryDeviceCallback: onDeviceFound || function () {}, + discoveryFinishedCallback: onDiscoveryFinished || function () {}, + }) + this.discoveryNewDevice() + }, + listenBluetoothStatus () { // #ifdef APP-PLUS - const btAdapter = getAdapter() - const activity = getActivity() - if (btAdapter && btAdapter.isDiscovering()) btAdapter.cancelDiscovery() - if (btFindReceiver && activity) { - try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} - btFindReceiver = null + if (activity == null) return + if (btStatusReceiver != null) { + try { activity.unregisterReceiver(btStatusReceiver) } catch (e) { console.error(e) } + btStatusReceiver = null + } + btStatusReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', { + onReceive: (context, intent) => { + plus.android.importClass(context) + plus.android.importClass(intent) + let action = intent.getAction() + if (action === BluetoothAdapter.ACTION_STATE_CHANGED) { + let blueState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0) + let stateStr = '' + switch (blueState) { + case BluetoothAdapter.STATE_TURNING_ON: + stateStr = 'STATE_TURNING_ON' + break + case BluetoothAdapter.STATE_ON: + stateStr = 'STATE_ON' + this.state.bluetoothEnable = true + break + case BluetoothAdapter.STATE_TURNING_OFF: + stateStr = 'STATE_TURNING_OFF' + break + case BluetoothAdapter.STATE_OFF: + stateStr = 'STATE_OFF' + this.state.bluetoothEnable = false + break + } + this.state.bluetoothState = stateStr + this.options.listenBTStatusCallback && this.options.listenBTStatusCallback(stateStr) + } + }, + }) + let filter = new IntentFilter() + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED) + activity.registerReceiver(btStatusReceiver, filter) + if (this.state.bluetoothEnable) { + this.options.listenBTStatusCallback && this.options.listenBTStatusCallback('STATE_ON') } // #endif }, - /** 连接经典蓝牙设备(含 D320FAX Virtual BT Printer,支持 RFCOMM 回退) */ connDevice (address, callback) { // #ifdef APP-PLUS + this.cancelDiscovery() if (btSocket != null) this.closeBtSocket() this.state.readThreadState = false - const btAdapter = getAdapter() - if (!btAdapter) { + this.state.connectionState = 'connecting' + this.state.lastAddress = String(address || '') + this.state.outputReady = false + this.clearErrorState() + + if (btAdapter == null) { this.shortToast('Bluetooth not available') - if (callback) callback(false) - return false - } - const inv = getInvoke() - const uuid = getMyUuid() - if (!inv || !uuid) { - this.shortToast('Bluetooth not ready') - if (callback) callback(false) + this.state.connectionState = 'error' + this.setErrorState('Bluetooth not available') + callback && callback(false) return false } try { - const device = inv(btAdapter, 'getRemoteDevice', address) - btSocket = inv(device, 'createRfcommSocketToServiceRecord', uuid) - } catch (e) { - console.warn('createRfcommSocketToServiceRecord failed, try fallback:', e) - try { - const device = inv(btAdapter, 'getRemoteDevice', address) - const cls = inv(device, 'getClass') - const intClass = plus.android.importClass('java.lang.Integer').TYPE - const m = inv(cls, 'getMethod', 'createRfcommSocket', intClass) - btSocket = inv(m, 'invoke', device, 1) - } catch (e2) { - console.error('RFCOMM fallback failed:', e2) - this.shortToast('Connect failed') - if (callback) callback(false) + let device = invoke(btAdapter, 'getRemoteDevice', address) + const candidates = createSocketCandidates(device) + let socket = null + let lastError = null + for (let i = 0; i < candidates.length; i++) { + const candidate = candidates[i] + try { + socket = candidate.create() + if (socket) { + this.state.lastSocketStrategy = candidate.name + break + } + } catch (e) { + lastError = e + console.warn('create socket failed:', candidate.name, e) + } + } + btSocket = socket + if (!btSocket) { + this.state.connectionState = 'error' + this.setErrorState(lastError ? getErrorMessage(lastError) : 'Unable to create Bluetooth socket') + callback && callback(false) return false } + } catch (e) { + console.error(e) + this.state.connectionState = 'error' + this.setErrorState(getErrorMessage(e)) + callback && callback(false) + return false } + try { - inv(btSocket, 'connect') - btInStream = inv(btSocket, 'getInputStream') - btOutStream = inv(btSocket, 'getOutputStream') - this.state.readThreadState = true - this.shortToast('Connected') - if (callback) callback(true) + invoke(btSocket, 'connect') + const streamReady = this.readData() + if (!streamReady) { + throw new Error(this.state.lastError || 'Bluetooth output stream not ready') + } + this.state.connectionState = 'connected' + this.shortToast('Classic Bluetooth connected') + callback && callback(true) } catch (e) { - try { btSocket.close() } catch (_) {} - btSocket = null - this.shortToast('Connect failed') - if (callback) callback(false) + console.error(e) + this.state.connectionState = 'error' + this.setErrorState(getErrorMessage(e)) + callback && callback(false) + try { + btSocket.close() + btSocket = null + } catch (e1) { + console.error(e1) + } + btInStream = null + btOutStream = null return false } return true // #endif - if (callback) callback(false) + callback && callback(false) return false }, disConnDevice () { // #ifdef APP-PLUS - this.closeBtSocket() + if (btSocket != null) this.closeBtSocket() this.state.readThreadState = false - this.shortToast('Disconnected') // #endif }, closeBtSocket () { // #ifdef APP-PLUS this.state.readThreadState = false - if (btSocket) { - try { btSocket.close() } catch (_) {} - btSocket = null - btInStream = null - btOutStream = null + this.state.outputReady = false + this.state.connectionState = 'idle' + clearInterval(setIntervalId) + setIntervalId = 0 + if (!btSocket) return + try { + btSocket.close() + } catch (e) { + console.error(e) } + btSocket = null + btInStream = null + btOutStream = null // #endif }, - sendByteData (byteData) { + cancelDiscovery () { + // #ifdef APP-PLUS + if (btAdapter != null && btAdapter.isDiscovering()) { + btAdapter.cancelDiscovery() + } + if (btFindReceiver != null && activity != null) { + try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} + btFindReceiver = null + } + this.state.discoveryDeviceState = false + // #endif + }, + cancelClassicDiscovery () { + this.cancelDiscovery() + }, + readData () { + // #ifdef APP-PLUS + if (!btSocket) { + this.shortToast('Please connect Bluetooth device first.') + this.state.connectionState = 'error' + this.setErrorState('Please connect Bluetooth device first.') + return false + } + try { + btInStream = invoke(btSocket, 'getInputStream') + btOutStream = invoke(btSocket, 'getOutputStream') + } catch (e) { + console.error(e) + this.setErrorState(getErrorMessage(e)) + this.closeBtSocket() + return false + } + this.read() + this.state.readThreadState = true + this.state.outputReady = !!btOutStream + return true + // #endif + return false + }, + read () { + // #ifdef APP-PLUS + clearInterval(setIntervalId) + setIntervalId = setInterval(() => { + if (this.state.readThreadState) { + let start = new Date().getTime() + let dataArr = [] + try { + while (btInStream && invoke(btInStream, 'available') !== 0) { + let data = invoke(btInStream, 'read') + dataArr.push(data) + let current = new Date().getTime() + if (current - start > 20) break + } + } catch (e) { + this.state.readThreadState = false + this.state.connectionState = 'error' + this.setErrorState(getErrorMessage(e)) + this.options.connExceptionCallback && this.options.connExceptionCallback(e) + } + if (dataArr.length > 0) { + this.options.readDataCallback && this.options.readDataCallback(dataArr) + } + } + }, 40) + // #endif + }, + sendData (dataStr) { // #ifdef APP-PLUS if (!btOutStream) { + this.shortToast('Output stream not ready') + return false + } + let bytes = invoke(dataStr, 'getBytes', 'gb18030') + try { + this.sendByteData(bytes) + } catch (e) { + return false + } + return true + // #endif + return false + }, + sendByteData (byteData) { + // #ifdef APP-PLUS + if (!this.ensureConnection(this.state.lastAddress)) { this.shortToast('Not connected') return false } try { - const CHUNK_SIZE = 4096 + const CHUNK_SIZE = 512 + this.state.lastSendMode = 'chunk-write' + this.state.lastSendError = '' for (let i = 0; i < byteData.length; i += CHUNK_SIZE) { - const chunk = byteData.slice(i, i + CHUNK_SIZE) - btOutStream.write(chunk) + const chunk = normalizeWriteChunk(byteData, i, Math.min(i + CHUNK_SIZE, byteData.length)) + try { + btOutStream.write(chunk) + } catch (writeChunkError) { + this.state.lastSendMode = 'byte-write-fallback' + for (let j = 0; j < chunk.length; j++) { + invoke(btOutStream, 'write', normalizeWriteByte(chunk[j])) + } + } } + invoke(btOutStream, 'flush') return true } catch (e) { + const message = getErrorMessage(e) + console.error('sendByteData failed:', e) + this.setErrorState(message, 'send') + this.state.connectionState = 'error' return false } // #endif return false - } + }, + isSocketConnected () { + // #ifdef APP-PLUS + if (!btSocket) return false + try { + return !!invoke(btSocket, 'isConnected') + } catch (_) { + return false + } + // #endif + return false + }, + ensureConnection (address) { + // #ifdef APP-PLUS + const targetAddress = String(address || this.state.lastAddress || '') + if (targetAddress) { + this.state.lastAddress = targetAddress + } + if (btOutStream && this.isSocketConnected()) { + this.state.outputReady = true + return true + } + if (!targetAddress) { + this.setErrorState('Bluetooth address missing') + return false + } + let connected = false + this.connDevice(targetAddress, (ok) => { + connected = !!ok + }) + return connected && !!btOutStream + // #endif + return false + }, + getLastError () { + return this.state.lastSendError || this.state.lastError || '' + }, + getDebugState () { + return { + ...this.state, + socketConnected: this.isSocketConnected(), + outputReady: !!btOutStream, + inputReady: !!btInStream, + } + }, } export default blueToothTool diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/drivers/d320fax.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/drivers/d320fax.ts new file mode 100644 index 0000000..f44dfce --- /dev/null +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/drivers/d320fax.ts @@ -0,0 +1,41 @@ +import { buildTscLabelData, buildTscTestPrintData } from '../protocols/tscProtocol' +import type { PrinterCandidate, PrinterDriver } from '../types/printer' + +const KEYWORDS = [ + 'd320fax', + 'd320fx', + 'virtual bt printer', + 'gp-d320fax', +] + +function score (device: PrinterCandidate): number { + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase() + let total = 0 + KEYWORDS.forEach((keyword) => { + if (text.includes(keyword)) total += 40 + }) + return total +} + +export const d320faxDriver: PrinterDriver = { + key: 'd320fax', + brand: 'Gprinter', + model: 'D320FAX/D320FX', + displayName: 'Gprinter D320FAX', + protocol: 'tsc', + preferredConnection: 'classic', + preferredBleMtu: 20, + keywords: KEYWORDS, + matches (device) { + return score(device) + }, + resolveConnectionType () { + return 'classic' + }, + buildTestPrintData () { + return buildTscTestPrintData() + }, + buildLabelData (payload) { + return buildTscLabelData(payload) + }, +} diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/drivers/genericTsc.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/drivers/genericTsc.ts new file mode 100644 index 0000000..ef4c427 --- /dev/null +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/drivers/genericTsc.ts @@ -0,0 +1,38 @@ +import { buildTscLabelData, buildTscTestPrintData } from '../protocols/tscProtocol' +import type { PrinterCandidate, PrinterDriver } from '../types/printer' + +const GENERIC_TSC_KEYWORDS = [ + 'printer', 'print', 'label', 'tsc', 'zebra', 'brother', 'epson', 'godex', + 'citizen', 'ql-', 'zd', 'zt', 'ttp', 'tdp', 'bt printer', 'virtual bt printer', +] + +function scoreByKeywords (device: PrinterCandidate, keywords: string[]): number { + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase() + let score = 0 + keywords.forEach((keyword) => { + if (text.includes(keyword)) score += 10 + }) + return score +} + +export const genericTscDriver: PrinterDriver = { + key: 'generic-tsc', + brand: 'Generic', + model: 'TSC', + displayName: 'Generic TSC Printer', + protocol: 'tsc', + preferredBleMtu: 20, + keywords: GENERIC_TSC_KEYWORDS, + matches (device) { + return scoreByKeywords(device, GENERIC_TSC_KEYWORDS) + }, + resolveConnectionType (device) { + return device.type === 'ble' ? 'ble' : 'classic' + }, + buildTestPrintData () { + return buildTscTestPrintData() + }, + buildLabelData (payload) { + return buildTscLabelData(payload) + }, +} diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/drivers/gpR3.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/drivers/gpR3.ts new file mode 100644 index 0000000..870804b --- /dev/null +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/drivers/gpR3.ts @@ -0,0 +1,42 @@ +import { buildEscPosLabelData, buildEscPosTestPrintData } from '../protocols/escPosBuilder' +import type { PrinterCandidate, PrinterDriver } from '../types/printer' + +const KEYWORDS = [ + 'gp-r3', + 'gp r3', + 'gpr3', +] + +function score (device: PrinterCandidate): number { + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase() + let total = 0 + KEYWORDS.forEach((keyword) => { + if (text.includes(keyword)) total += 60 + }) + if (text.includes('gprinter')) total += 10 + return total +} + +export const gpR3Driver: PrinterDriver = { + key: 'gp-r3', + brand: 'Gprinter', + model: 'GP-R3', + displayName: 'Gprinter GP-R3', + protocol: 'esc', + preferredConnection: 'classic', + preferredBleMtu: 20, + keywords: KEYWORDS, + matches (device) { + return score(device) + }, + resolveConnectionType (device) { + if (device.type === 'ble') return 'ble' + return 'classic' + }, + buildTestPrintData () { + return buildEscPosTestPrintData() + }, + buildLabelData (payload) { + return buildEscPosLabelData(payload) + }, +} diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/manager/driverRegistry.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/manager/driverRegistry.ts new file mode 100644 index 0000000..3e27fd0 --- /dev/null +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/manager/driverRegistry.ts @@ -0,0 +1,42 @@ +import { d320faxDriver } from '../drivers/d320fax' +import { genericTscDriver } from '../drivers/genericTsc' +import { gpR3Driver } from '../drivers/gpR3' +import type { PrinterCandidate, PrinterDriver, ResolvedPrinterCandidate } from '../types/printer' + +const printerDrivers: PrinterDriver[] = [ + gpR3Driver, + d320faxDriver, + genericTscDriver, +] + +export function getPrinterDrivers (): PrinterDriver[] { + return printerDrivers +} + +export function getPrinterDriverByKey (key?: string): PrinterDriver { + return printerDrivers.find(driver => driver.key === key) || genericTscDriver +} + +export function resolvePrinterDriver (device: PrinterCandidate): PrinterDriver { + let bestDriver: PrinterDriver = genericTscDriver + let bestScore = -1 + printerDrivers.forEach((driver) => { + const score = driver.matches(device) + if (score > bestScore) { + bestScore = score + bestDriver = driver + } + }) + return bestDriver +} + +export function describePrinterCandidate (device: PrinterCandidate): ResolvedPrinterCandidate { + const driver = resolvePrinterDriver(device) + return { + ...device, + driverKey: driver.key, + driverName: driver.displayName, + protocol: driver.protocol, + resolvedType: driver.resolveConnectionType(device), + } +} diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts new file mode 100644 index 0000000..359ed2b --- /dev/null +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts @@ -0,0 +1,243 @@ +import { + clearPrinter, + getBluetoothConnection, + getCurrentPrinterDriverKey, + getPrinterType, + sendToPrinter, + setBluetoothConnection, + setBuiltinPrinter, +} from '../printerConnection' +import classicBluetooth from '../bluetoothTool.js' +import { describePrinterCandidate, getPrinterDriverByKey, resolvePrinterDriver } from './driverRegistry' +import type { + CurrentPrinterSummary, + LabelPrintPayload, + PrinterCandidate, + PrinterDriver, +} from '../types/printer' + +function connectClassicBluetooth (device: PrinterCandidate, driver: PrinterDriver): Promise { + return new Promise((resolve, reject) => { + // #ifdef APP-PLUS + const classic = classicBluetooth + if (!classic || !classic.connDevice) { + reject(new Error('Classic Bluetooth not available. Ensure app is running on the device.')) + return + } + classic.connDevice(device.deviceId, (ok: boolean) => { + if (!ok) { + reject(new Error('Classic Bluetooth connection failed.')) + return + } + setBluetoothConnection({ + deviceId: device.deviceId, + deviceName: device.name || 'Bluetooth Printer', + deviceType: 'classic', + driverKey: driver.key, + mtu: driver.preferredBleMtu || 20, + }) + resolve() + }) + // #endif + // #ifndef APP-PLUS + reject(new Error('Classic Bluetooth requires the app.')) + // #endif + }) +} + +function findBleWriteCharacteristic (deviceId: string): Promise<{ serviceId: string; characteristicId: string } | null> { + return new Promise((resolve) => { + uni.getBLEDeviceServices({ + deviceId, + success: (serviceRes) => { + const services = serviceRes.services || [] + const next = (index: number) => { + if (index >= services.length) { + resolve(null) + return + } + const serviceId = services[index].uuid + uni.getBLEDeviceCharacteristics({ + deviceId, + serviceId, + success: (charRes) => { + const target = (charRes.characteristics || []).find((item: any) => item.properties && item.properties.write) + if (target) { + resolve({ + serviceId, + characteristicId: target.uuid, + }) + return + } + next(index + 1) + }, + fail: () => next(index + 1), + }) + } + next(0) + }, + fail: () => resolve(null), + }) + }) +} + +function connectBlePrinter (device: PrinterCandidate, driver: PrinterDriver): Promise { + const finalizeExistingBleConnection = async () => { + const write = await findBleWriteCharacteristic(device.deviceId) + if (!write) { + throw new Error('No writable characteristic found. This device may not support printing.') + } + setBluetoothConnection({ + deviceId: device.deviceId, + deviceName: device.name || 'Bluetooth Printer', + serviceId: write.serviceId, + characteristicId: write.characteristicId, + deviceType: 'ble', + mtu: driver.preferredBleMtu || 20, + driverKey: driver.key, + }) + } + + return new Promise((resolve, reject) => { + uni.createBLEConnection({ + deviceId: device.deviceId, + timeout: 10000, + success: async () => { + try { + await finalizeExistingBleConnection() + resolve() + } catch (e: any) { + reject(e) + } + }, + fail: (err: any) => { + if (err?.errCode === -1) { + finalizeExistingBleConnection().then(() => resolve()).catch(reject) + } else { + reject(new Error(err?.errMsg || 'BLE connection failed.')) + } + }, + }) + }) +} + +export async function connectBluetoothPrinter (device: PrinterCandidate): Promise { + const driver = resolvePrinterDriver(device) + const resolvedType = driver.resolveConnectionType(device) + if (resolvedType === 'classic') { + await connectClassicBluetooth(device, driver) + } else { + await connectBlePrinter(device, driver) + } + return driver +} + +export function useBuiltinPrinter (driverKey = 'generic-tsc') { + setBuiltinPrinter(driverKey) +} + +export function getCurrentPrinterDriver (): PrinterDriver { + const type = getPrinterType() + const storedKey = getCurrentPrinterDriverKey() + if (storedKey) return getPrinterDriverByKey(storedKey) + if (type === 'bluetooth') { + const connection = getBluetoothConnection() + if (connection) { + return resolvePrinterDriver({ + deviceId: connection.deviceId, + name: connection.deviceName, + type: connection.deviceType, + }) + } + } + return getPrinterDriverByKey('generic-tsc') +} + +export function getCurrentPrinterSummary (): CurrentPrinterSummary { + const type = getPrinterType() + const driver = getCurrentPrinterDriver() + if (type === 'builtin') { + return { + type, + displayName: 'Built-in Printer', + deviceId: 'builtin', + driverKey: driver.key, + driverName: driver.displayName, + protocol: driver.protocol, + deviceType: '', + } + } + if (type === 'bluetooth') { + const connection = getBluetoothConnection() + if (connection) { + return { + type, + displayName: connection.deviceName || driver.displayName, + deviceId: connection.deviceId, + driverKey: driver.key, + driverName: driver.displayName, + protocol: driver.protocol, + deviceType: connection.deviceType, + } + } + } + return { + type: '', + displayName: '', + deviceId: '', + driverKey: driver.key, + driverName: driver.displayName, + protocol: driver.protocol, + deviceType: '', + } +} + +export async function testPrintCurrentPrinter (onProgress?: (percent: number) => void): Promise { + const driver = getCurrentPrinterDriver() + await sendToPrinter(driver.buildTestPrintData(), onProgress) + return driver +} + +export async function printLabelForCurrentPrinter ( + payload: LabelPrintPayload, + onProgress?: (percent: number) => void +): Promise { + const driver = getCurrentPrinterDriver() + await sendToPrinter(driver.buildLabelData(payload), onProgress) + return driver +} + +export function describeDiscoveredPrinter (device: PrinterCandidate) { + return describePrinterCandidate(device) +} + +export function disconnectCurrentPrinter (): Promise { + return new Promise((resolve) => { + const type = getPrinterType() + const connection = getBluetoothConnection() + + if (type === 'bluetooth' && connection?.deviceType === 'classic') { + // #ifdef APP-PLUS + try { + const classic = classicBluetooth + if (classic && classic.disConnDevice) classic.disConnDevice() + } catch (e) { + console.error('Disconnect classic bluetooth failed', e) + } + // #endif + clearPrinter() + resolve() + return + } + + clearPrinter() + if (type === 'bluetooth' && connection?.deviceId) { + uni.closeBLEConnection({ + deviceId: connection.deviceId, + complete: () => resolve(), + }) + return + } + resolve() + }) +} diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts index 286c09b..b14ca66 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts @@ -1,6 +1,8 @@ /** * 打印机连接与下发:蓝牙(BLE) / 一体机(TCP localhost) */ +import type { ActiveBtDeviceType, PrinterType } from './types/printer' +import classicBluetooth from './bluetoothTool.js' const STORAGE_PRINTER_TYPE = 'printerType' const STORAGE_BT_DEVICE_ID = 'btDeviceId' @@ -10,11 +12,10 @@ 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] - -export type PrinterType = 'bluetooth' | 'builtin' -export type BtDeviceType = 'ble' | 'classic' +export type BtDeviceType = ActiveBtDeviceType export const PrinterStorageKeys = { type: STORAGE_PRINTER_TYPE, @@ -24,6 +25,7 @@ export const PrinterStorageKeys = { 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) { @@ -37,6 +39,7 @@ export function setBluetoothConnection (info: { characteristicId?: string deviceType?: BtDeviceType mtu?: number + driverKey?: string }) { uni.setStorageSync(STORAGE_PRINTER_TYPE, 'bluetooth') uni.setStorageSync(STORAGE_BT_DEVICE_ID, info.deviceId) @@ -45,10 +48,12 @@ export function setBluetoothConnection (info: { 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 () { +export function setBuiltinPrinter (driverKey = 'generic-tsc') { uni.setStorageSync(STORAGE_PRINTER_TYPE, 'builtin') + uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, driverKey) } export function clearPrinter () { @@ -60,6 +65,7 @@ export function clearPrinter () { 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 @@ -68,6 +74,10 @@ export function getPrinterType (): PrinterType | '' { return (uni.getStorageSync(STORAGE_PRINTER_TYPE) as PrinterType) || '' } +export function getCurrentPrinterDriverKey (): string { + return String(uni.getStorageSync(STORAGE_PRINTER_DRIVER_KEY) || '') +} + export function getBluetoothConnection (): { deviceId: string deviceName: string @@ -186,20 +196,31 @@ function sendViaClassic ( } return new Promise((resolve, reject) => { try { - const classicBluetooth = (require('./bluetoothTool.js') as any).default if (!classicBluetooth) { reject(new Error('Classic Bluetooth not available')) return } - const sendData = data.map((byte) => { - const b = byte & 0xff - if (b >= 128) return b % 128 - 128 - return b - }) + const ready = typeof classicBluetooth.ensureConnection === 'function' + ? classicBluetooth.ensureConnection(conn.deviceId) + : true + if (!ready) { + const errorMessage = typeof classicBluetooth.getLastError === 'function' + ? classicBluetooth.getLastError() + : '' + reject(new Error(errorMessage || 'Classic Bluetooth connection is not ready')) + return + } + + const sendData = data.map((byte) => byte & 0xff) const ok = classicBluetooth.sendByteData(sendData) if (onProgress) onProgress(100) if (ok) resolve() - else reject(new Error('Classic Bluetooth send failed')) + else { + const errorMessage = typeof classicBluetooth.getLastError === 'function' + ? classicBluetooth.getLastError() + : '' + reject(new Error(errorMessage || 'Classic Bluetooth send failed')) + } } catch (e: any) { reject(e) } diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts new file mode 100644 index 0000000..348056b --- /dev/null +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts @@ -0,0 +1,100 @@ +import type { LabelPrintPayload } from '../types/printer' + +function stringToBytes (str: string): number[] { + const out: number[] = [] + for (let i = 0; i < str.length; i++) { + let c = str.charCodeAt(i) + if (c < 0x80) { + out.push(c) + } else if (c < 0x800) { + out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)) + } else if (c < 0xd800 || c >= 0xe000) { + out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)) + } else { + i++ + const c2 = str.charCodeAt(i) + const u = ((c & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000 + out.push( + 0xf0 | (u >> 18), + 0x80 | ((u >> 12) & 0x3f), + 0x80 | ((u >> 6) & 0x3f), + 0x80 | (u & 0x3f) + ) + } + } + return out +} + +function appendText (out: number[], text: string) { + const bytes = stringToBytes(text) + for (let i = 0; i < bytes.length; i++) out.push(bytes[i]) +} + +function appendLine (out: number[], text = '') { + appendText(out, text) + out.push(0x0a) +} + +function appendAlign (out: number[], align: 0 | 1 | 2) { + out.push(0x1b, 0x61, align) +} + +function appendBold (out: number[], bold: boolean) { + out.push(0x1b, 0x45, bold ? 1 : 0) +} + +function appendSize (out: number[], width = 0, height = 0) { + const value = ((width & 0x07) << 4) | (height & 0x07) + out.push(0x1d, 0x21, value) +} + +function createEscDocument (builder: (out: number[]) => void): number[] { + const out: number[] = [] + out.push(0x1b, 0x40) + builder(out) + out.push(0x1b, 0x64, 0x04) + return out +} + +export function buildEscPosTestPrintData (): number[] { + return createEscDocument((out) => { + appendAlign(out, 1) + appendBold(out, true) + appendSize(out, 1, 1) + appendLine(out, 'TEST PRINT') + appendBold(out, false) + appendSize(out, 0, 0) + appendLine(out, 'GP-R3 / ESC-POS') + appendLine(out, 'Food Label System') + appendLine(out, '-----------------------------') + appendLine(out, 'Connection OK') + appendLine(out, 'Protocol OK') + appendLine(out, '-----------------------------') + }) +} + +export function buildEscPosLabelData (payload: LabelPrintPayload): number[] { + const { + productName, + labelId, + printQty = 1, + category = '', + extraLine = '', + } = payload + + return createEscDocument((out) => { + appendAlign(out, 1) + appendBold(out, true) + appendSize(out, 1, 1) + appendLine(out, 'FOOD LABEL') + appendBold(out, false) + appendSize(out, 0, 0) + appendLine(out, '-----------------------------') + appendLine(out, 'Product: ' + productName) + if (category) appendLine(out, 'Category: ' + category) + appendLine(out, 'Label ID: ' + labelId) + if (extraLine) appendLine(out, extraLine) + appendLine(out, 'Qty: ' + String(printQty)) + appendLine(out, '-----------------------------') + }) +} diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/tscProtocol.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/tscProtocol.ts new file mode 100644 index 0000000..0d90e9f --- /dev/null +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/protocols/tscProtocol.ts @@ -0,0 +1,10 @@ +import type { LabelPrintPayload } from '../types/printer' +import { buildTestTscLabel, buildTscLabel } from '../tscLabelBuilder' + +export function buildTscTestPrintData (): number[] { + return buildTestTscLabel() +} + +export function buildTscLabelData (payload: LabelPrintPayload): number[] { + return buildTscLabel(payload) +} diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts new file mode 100644 index 0000000..34d7e9b --- /dev/null +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts @@ -0,0 +1,53 @@ +export type PrinterType = 'bluetooth' | 'builtin' +export type BtDeviceType = 'ble' | 'classic' | 'dual' | 'unknown' +export type ActiveBtDeviceType = 'ble' | 'classic' +export type PrinterProtocol = 'tsc' | 'esc' + +export interface PrinterCandidate { + deviceId: string + name: string + RSSI?: number + type?: BtDeviceType +} + +export interface LabelPrintPayload { + productName: string + labelId: string + printQty?: number + widthMm?: number + heightMm?: number + category?: string + extraLine?: string +} + +export interface PrinterDriver { + key: string + brand: string + model: string + displayName: string + protocol: PrinterProtocol + preferredConnection?: ActiveBtDeviceType + preferredBleMtu?: number + keywords: string[] + matches: (device: PrinterCandidate) => number + resolveConnectionType: (device: PrinterCandidate) => ActiveBtDeviceType + buildTestPrintData: () => number[] + buildLabelData: (payload: LabelPrintPayload) => number[] +} + +export interface ResolvedPrinterCandidate extends PrinterCandidate { + driverKey: string + driverName: string + protocol: PrinterProtocol + resolvedType: ActiveBtDeviceType +} + +export interface CurrentPrinterSummary { + type: PrinterType | '' + displayName: string + deviceId: string + driverKey: string + driverName: string + protocol: PrinterProtocol + deviceType?: ActiveBtDeviceType | '' +} -- libgit2 0.21.4