Commit 961eecae3d8a442b5739a022bebd769929b22978
1 parent
0afa2a00
对打印机进行开发
Showing
15 changed files
with
1529 additions
and
494 deletions
美国版/Food Labeling Management App UniApp/src/manifest.json
| ... | ... | @@ -2,8 +2,8 @@ |
| 2 | 2 | "name" : "food.labeling", |
| 3 | 3 | "appid" : "__UNI__1BFD76D", |
| 4 | 4 | "description" : "", |
| 5 | - "versionName" : "1.0.1", | |
| 6 | - "versionCode" : 101, | |
| 5 | + "versionName" : "1.0.3", | |
| 6 | + "versionCode" : 103, | |
| 7 | 7 | "transformPx" : false, |
| 8 | 8 | /* 5+App特有相关 */ |
| 9 | 9 | "app-plus" : { |
| ... | ... | @@ -18,8 +18,9 @@ |
| 18 | 18 | }, |
| 19 | 19 | /* 模块配置 */ |
| 20 | 20 | "modules" : { |
| 21 | - "Bluetooth" : {}, | |
| 22 | - "Camera" : {} | |
| 21 | + "Camera" : {}, | |
| 22 | + "Barcode" : {}, | |
| 23 | + "Bluetooth" : {} | |
| 23 | 24 | }, |
| 24 | 25 | /* 应用发布信息 */ |
| 25 | 26 | "distribute" : { |
| ... | ... | @@ -43,6 +44,10 @@ |
| 43 | 44 | "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>", |
| 44 | 45 | "<uses-permission android:name=\"android.permission.BLUETOOTH\"/>", |
| 45 | 46 | "<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>", |
| 47 | + "<uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\"/>", | |
| 48 | + "<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\"/>", | |
| 49 | + "<uses-permission android:name=\"android.permission.BLUETOOTH_ADVERTISE\"/>", | |
| 50 | + "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>", | |
| 46 | 51 | "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>" |
| 47 | 52 | ] |
| 48 | 53 | }, | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue
| ... | ... | @@ -42,6 +42,20 @@ |
| 42 | 42 | <text class="error-text">{{ errorMsg }}</text> |
| 43 | 43 | </view> |
| 44 | 44 | |
| 45 | + <view class="debug-card"> | |
| 46 | + <text class="debug-title">Debug Status</text> | |
| 47 | + <text class="debug-item">Current Mode: {{ debugInfo.currentMode }}</text> | |
| 48 | + <text class="debug-item">Classic Module: {{ debugInfo.classicModuleReady ? 'Ready' : 'Not Ready' }}</text> | |
| 49 | + <text class="debug-item">Paired Count: {{ debugInfo.pairedCount }}</text> | |
| 50 | + <text class="debug-item">Virtual BT Printer: {{ debugInfo.foundVirtualPrinter ? 'Found' : 'Not Found' }}</text> | |
| 51 | + <text class="debug-item">Classic Scan: {{ debugInfo.lastClassicEvent }}</text> | |
| 52 | + <text class="debug-item">BLE Scan: {{ debugInfo.lastBleEvent }}</text> | |
| 53 | + <text v-if="debugInfo.lastBleError" class="debug-item debug-error">{{ debugInfo.lastBleError }}</text> | |
| 54 | + <text v-if="debugInfo.locationServiceRequired" class="debug-item debug-warn"> | |
| 55 | + Android system Location service is OFF. Turn it on before BLE scan. | |
| 56 | + </text> | |
| 57 | + </view> | |
| 58 | + | |
| 45 | 59 | <!-- Connected --> |
| 46 | 60 | <view v-if="connectedDevice" class="connected-card"> |
| 47 | 61 | <view class="connected-header"> |
| ... | ... | @@ -153,26 +167,21 @@ import AppIcon from '../../components/AppIcon.vue' |
| 153 | 167 | import SideMenu from '../../components/SideMenu.vue' |
| 154 | 168 | import LocationPicker from '../../components/LocationPicker.vue' |
| 155 | 169 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 170 | +import classicBluetooth from '../../utils/print/bluetoothTool.js' | |
| 156 | 171 | import { |
| 157 | 172 | getPrinterType, |
| 158 | - getBluetoothConnection, | |
| 159 | - setBluetoothConnection, | |
| 160 | - setBuiltinPrinter, | |
| 161 | 173 | clearPrinter, |
| 162 | - sendToPrinter, | |
| 163 | 174 | type PrinterType, |
| 164 | 175 | } from '../../utils/print/printerConnection' |
| 165 | -import { buildTestTscLabel } from '../../utils/print/tscLabelBuilder' | |
| 166 | - | |
| 167 | -function getClassicBluetooth () { | |
| 168 | - try { | |
| 169 | - // @ts-ignore - dynamic require for native plugin | |
| 170 | - const m = require('../../utils/print/bluetoothTool.js') | |
| 171 | - return m?.default ?? null | |
| 172 | - } catch { | |
| 173 | - return null | |
| 174 | - } | |
| 175 | -} | |
| 176 | +import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions' | |
| 177 | +import { | |
| 178 | + connectBluetoothPrinter, | |
| 179 | + describeDiscoveredPrinter, | |
| 180 | + disconnectCurrentPrinter, | |
| 181 | + getCurrentPrinterSummary, | |
| 182 | + testPrintCurrentPrinter, | |
| 183 | + useBuiltinPrinter, | |
| 184 | +} from '../../utils/print/manager/printerManager' | |
| 176 | 185 | |
| 177 | 186 | const statusBarHeight = getStatusBarHeight() |
| 178 | 187 | const isMenuOpen = ref(false) |
| ... | ... | @@ -181,6 +190,17 @@ const connectingId = ref('') |
| 181 | 190 | const errorMsg = ref('') |
| 182 | 191 | const btAdapterReady = ref(false) |
| 183 | 192 | const printerType = ref<PrinterType | ''>(getPrinterType() || 'bluetooth') |
| 193 | +const currentPrinter = ref(getCurrentPrinterSummary()) | |
| 194 | +const debugInfo = ref({ | |
| 195 | + currentMode: 'none', | |
| 196 | + classicModuleReady: false, | |
| 197 | + pairedCount: 0, | |
| 198 | + foundVirtualPrinter: false, | |
| 199 | + lastClassicEvent: 'idle', | |
| 200 | + lastBleEvent: 'idle', | |
| 201 | + lastBleError: '', | |
| 202 | + locationServiceRequired: false, | |
| 203 | +}) | |
| 184 | 204 | |
| 185 | 205 | interface BtDevice { |
| 186 | 206 | deviceId: string |
| ... | ... | @@ -192,14 +212,26 @@ interface BtDevice { |
| 192 | 212 | const devices = ref<BtDevice[]>([]) |
| 193 | 213 | const discoveredIds = new Set<string>() |
| 194 | 214 | |
| 215 | +function refreshCurrentPrinter () { | |
| 216 | + currentPrinter.value = getCurrentPrinterSummary() | |
| 217 | + debugInfo.value.currentMode = currentPrinter.value.type || 'none' | |
| 218 | +} | |
| 219 | + | |
| 220 | +function hasPreferredClassicDeviceInList () { | |
| 221 | + return devices.value.some((item: any) => { | |
| 222 | + const name = String(item?.name || '').toLowerCase() | |
| 223 | + const type = String(item?.type || '').toLowerCase() | |
| 224 | + return name.includes('virtual bt printer') || type === 'classic' || type === 'dual' | |
| 225 | + }) | |
| 226 | +} | |
| 227 | + | |
| 195 | 228 | const connectedDevice = computed(() => { |
| 196 | - const type = getPrinterType() | |
| 197 | - if (printerType.value === 'builtin' && type === 'builtin') { | |
| 198 | - return { name: 'Built-in Printer', deviceId: 'builtin' } | |
| 229 | + const summary = currentPrinter.value | |
| 230 | + if (printerType.value === 'builtin' && summary.type === 'builtin') { | |
| 231 | + return { name: summary.displayName, deviceId: summary.deviceId } | |
| 199 | 232 | } |
| 200 | - if (printerType.value === 'bluetooth' && type === 'bluetooth') { | |
| 201 | - const conn = getBluetoothConnection() | |
| 202 | - if (conn) return { name: conn.deviceName, deviceId: conn.deviceId } | |
| 233 | + if (printerType.value === 'bluetooth' && summary.type === 'bluetooth') { | |
| 234 | + return { name: summary.displayName, deviceId: summary.deviceId } | |
| 203 | 235 | } |
| 204 | 236 | return null |
| 205 | 237 | }) |
| ... | ... | @@ -208,6 +240,7 @@ function switchType (type: 'bluetooth' | 'builtin') { |
| 208 | 240 | printerType.value = type |
| 209 | 241 | if (type === 'bluetooth' && getPrinterType() === 'builtin') { |
| 210 | 242 | clearPrinter() |
| 243 | + refreshCurrentPrinter() | |
| 211 | 244 | } |
| 212 | 245 | } |
| 213 | 246 | |
| ... | ... | @@ -240,9 +273,16 @@ const startBleScan = () => { |
| 240 | 273 | success: () => { |
| 241 | 274 | isScanning.value = true |
| 242 | 275 | errorMsg.value = '' |
| 276 | + debugInfo.value.lastBleEvent = 'scan running' | |
| 243 | 277 | }, |
| 244 | 278 | fail: (err: any) => { |
| 245 | 279 | console.error('BLE startBluetoothDevicesDiscovery fail:', err) |
| 280 | + debugInfo.value.lastBleEvent = 'scan failed' | |
| 281 | + debugInfo.value.lastBleError = err?.errMsg || 'BLE scan failed' | |
| 282 | + if (err?.errCode === 10016 || err?.code === 10016) { | |
| 283 | + debugInfo.value.locationServiceRequired = true | |
| 284 | + errorMsg.value = 'Bluetooth scan failed: Android system Location service is turned off.' | |
| 285 | + } | |
| 246 | 286 | }, |
| 247 | 287 | }) |
| 248 | 288 | } |
| ... | ... | @@ -254,7 +294,7 @@ const stopDiscovery = () => { |
| 254 | 294 | }, |
| 255 | 295 | }) |
| 256 | 296 | // #ifdef APP-PLUS |
| 257 | - const classic = getClassicBluetooth() | |
| 297 | + const classic = classicBluetooth | |
| 258 | 298 | if (classic && classic.cancelClassicDiscovery) classic.cancelClassicDiscovery() |
| 259 | 299 | // #endif |
| 260 | 300 | } |
| ... | ... | @@ -266,12 +306,12 @@ const onDeviceFound = (res: any) => { |
| 266 | 306 | const name = (d.localName || d.name || '').trim() |
| 267 | 307 | const displayName = name || 'Unknown Device' |
| 268 | 308 | discoveredIds.add(d.deviceId) |
| 269 | - devices.value.push({ | |
| 309 | + devices.value.push(describeDiscoveredPrinter({ | |
| 270 | 310 | deviceId: d.deviceId, |
| 271 | 311 | name: displayName, |
| 272 | 312 | RSSI: d.RSSI, |
| 273 | 313 | type: 'ble', |
| 274 | - }) | |
| 314 | + })) | |
| 275 | 315 | } |
| 276 | 316 | devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) |
| 277 | 317 | } |
| ... | ... | @@ -285,12 +325,12 @@ function mergeCachedBleDevices () { |
| 285 | 325 | if (discoveredIds.has(d.deviceId)) continue |
| 286 | 326 | const name = (d.localName || d.name || '').trim() |
| 287 | 327 | discoveredIds.add(d.deviceId) |
| 288 | - devices.value.push({ | |
| 328 | + devices.value.push(describeDiscoveredPrinter({ | |
| 289 | 329 | deviceId: d.deviceId, |
| 290 | 330 | name: name || 'Unknown Device', |
| 291 | 331 | RSSI: d.RSSI, |
| 292 | 332 | type: 'ble', |
| 293 | - }) | |
| 333 | + })) | |
| 294 | 334 | } |
| 295 | 335 | if (list.length > 0) devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) |
| 296 | 336 | }, |
| ... | ... | @@ -299,104 +339,98 @@ function mergeCachedBleDevices () { |
| 299 | 339 | |
| 300 | 340 | function addPairedDevices () { |
| 301 | 341 | // #ifdef APP-PLUS |
| 302 | - const classic = getClassicBluetooth() | |
| 342 | + const classic = classicBluetooth | |
| 303 | 343 | if (!classic || !classic.getPairedDevices) return |
| 304 | 344 | try { |
| 305 | 345 | const paired = classic.getPairedDevices() |
| 346 | + debugInfo.value.pairedCount = (paired || []).length | |
| 347 | + debugInfo.value.foundVirtualPrinter = (paired || []).some((item: any) => String(item?.name || '').toLowerCase().includes('virtual bt printer')) | |
| 306 | 348 | for (const p of paired) { |
| 307 | 349 | if (discoveredIds.has(p.deviceId)) continue |
| 308 | 350 | discoveredIds.add(p.deviceId) |
| 309 | - devices.value.push({ | |
| 351 | + devices.value.push(describeDiscoveredPrinter({ | |
| 310 | 352 | deviceId: p.deviceId, |
| 311 | - name: p.name || 'Unknown', | |
| 353 | + name: p.name || 'Unknown Device', | |
| 312 | 354 | type: p.type || 'classic', |
| 313 | - }) | |
| 355 | + })) | |
| 314 | 356 | } |
| 315 | 357 | if (paired.length > 0) { |
| 358 | + debugInfo.value.lastClassicEvent = 'paired devices loaded' | |
| 316 | 359 | devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) |
| 360 | + } else { | |
| 361 | + debugInfo.value.lastClassicEvent = 'no paired devices' | |
| 317 | 362 | } |
| 318 | 363 | } catch (e) { |
| 319 | 364 | console.error('addPairedDevices error:', e) |
| 365 | + debugInfo.value.lastClassicEvent = 'load paired devices failed' | |
| 320 | 366 | } |
| 321 | 367 | // #endif |
| 322 | 368 | } |
| 323 | 369 | |
| 324 | -function findWriteCharacteristic (deviceId: string): Promise<{ serviceId: string; characteristicId: string } | null> { | |
| 325 | - return new Promise((resolve) => { | |
| 326 | - uni.getBLEDeviceServices({ | |
| 327 | - deviceId, | |
| 328 | - success: (sres) => { | |
| 329 | - const services = sres.services || [] | |
| 330 | - const tryNext = (idx: number) => { | |
| 331 | - if (idx >= services.length) { | |
| 332 | - resolve(null) | |
| 333 | - return | |
| 334 | - } | |
| 335 | - const serviceId = services[idx].uuid | |
| 336 | - uni.getBLEDeviceCharacteristics({ | |
| 337 | - deviceId, | |
| 338 | - serviceId, | |
| 339 | - success: (cres) => { | |
| 340 | - const chars = cres.characteristics || [] | |
| 341 | - const writeChar = chars.find((c: any) => c.properties && c.properties.write) | |
| 342 | - if (writeChar) { | |
| 343 | - resolve({ serviceId, characteristicId: writeChar.uuid }) | |
| 344 | - return | |
| 345 | - } | |
| 346 | - tryNext(idx + 1) | |
| 347 | - }, | |
| 348 | - fail: () => tryNext(idx + 1), | |
| 349 | - }) | |
| 350 | - } | |
| 351 | - tryNext(0) | |
| 352 | - }, | |
| 353 | - fail: () => resolve(null), | |
| 354 | - }) | |
| 355 | - }) | |
| 356 | -} | |
| 357 | - | |
| 358 | -const handleScan = async () => { | |
| 359 | - if (isScanning.value) { | |
| 360 | - stopDiscovery() | |
| 361 | - return | |
| 362 | - } | |
| 363 | - errorMsg.value = '' | |
| 364 | - devices.value = [] | |
| 365 | - discoveredIds.clear() | |
| 366 | - | |
| 367 | - addPairedDevices() | |
| 368 | - | |
| 370 | +function startClassicScan () { | |
| 369 | 371 | // #ifdef APP-PLUS |
| 370 | - const classic = getClassicBluetooth() | |
| 372 | + const classic = classicBluetooth | |
| 371 | 373 | if (classic && classic.startClassicDiscovery) { |
| 372 | 374 | try { |
| 373 | 375 | classic.startClassicDiscovery( |
| 374 | 376 | (dev: { name: string; deviceId: string; type: string }) => { |
| 377 | + debugInfo.value.lastClassicEvent = 'device found' | |
| 375 | 378 | if (discoveredIds.has(dev.deviceId)) return |
| 376 | 379 | discoveredIds.add(dev.deviceId) |
| 377 | - devices.value.push({ | |
| 380 | + devices.value.push(describeDiscoveredPrinter({ | |
| 378 | 381 | deviceId: dev.deviceId, |
| 379 | - name: dev.name || 'Unknown', | |
| 382 | + name: dev.name || 'Unknown Device', | |
| 380 | 383 | type: (dev.type as BtDevice['type']) || 'classic', |
| 381 | - }) | |
| 384 | + })) | |
| 385 | + devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) | |
| 386 | + }, | |
| 387 | + () => { | |
| 388 | + debugInfo.value.lastClassicEvent = 'scan finished' | |
| 382 | 389 | }, |
| 383 | - () => {}, | |
| 384 | 390 | ) |
| 385 | 391 | isScanning.value = true |
| 392 | + debugInfo.value.lastClassicEvent = 'scan running' | |
| 386 | 393 | } catch (e) { |
| 387 | 394 | console.error('Classic discovery failed', e) |
| 395 | + debugInfo.value.lastClassicEvent = 'scan failed' | |
| 388 | 396 | } |
| 397 | + } else { | |
| 398 | + debugInfo.value.lastClassicEvent = 'classic module unavailable' | |
| 389 | 399 | } |
| 390 | 400 | // #endif |
| 401 | +} | |
| 391 | 402 | |
| 392 | - if (devices.value.length > 0) { | |
| 393 | - uni.showToast({ title: `Found ${devices.value.length} device(s)`, icon: 'none' }) | |
| 403 | +const handleScan = async () => { | |
| 404 | + if (isScanning.value) { | |
| 405 | + stopDiscovery() | |
| 406 | + return | |
| 394 | 407 | } |
| 408 | + errorMsg.value = '' | |
| 409 | + devices.value = [] | |
| 410 | + discoveredIds.clear() | |
| 411 | + debugInfo.value.lastBleError = '' | |
| 412 | + debugInfo.value.locationServiceRequired = false | |
| 413 | + debugInfo.value.lastClassicEvent = 'starting' | |
| 414 | + debugInfo.value.lastBleEvent = 'starting' | |
| 395 | 415 | |
| 396 | 416 | try { |
| 397 | - if (!btAdapterReady.value) { | |
| 398 | - await initBluetooth() | |
| 417 | + const permissionResult = await ensureBluetoothPermissions({ scan: true, connect: true }) | |
| 418 | + if (!permissionResult.ok) { | |
| 419 | + errorMsg.value = permissionResult.message || 'Bluetooth permission denied.' | |
| 420 | + return | |
| 399 | 421 | } |
| 422 | + await initBluetooth() | |
| 423 | + | |
| 424 | + addPairedDevices() | |
| 425 | + startClassicScan() | |
| 426 | + | |
| 427 | + if (hasPreferredClassicDeviceInList()) { | |
| 428 | + isScanning.value = false | |
| 429 | + debugInfo.value.lastBleEvent = 'skipped (paired classic device found)' | |
| 430 | + uni.showToast({ title: 'Using paired classic Bluetooth devices', icon: 'none' }) | |
| 431 | + return | |
| 432 | + } | |
| 433 | + | |
| 400 | 434 | mergeCachedBleDevices() |
| 401 | 435 | await new Promise<void>((resolve) => { |
| 402 | 436 | uni.getLocation({ |
| ... | ... | @@ -424,77 +458,27 @@ const handleConnect = async (dev: BtDevice) => { |
| 424 | 458 | |
| 425 | 459 | if (isScanning.value) stopDiscovery() |
| 426 | 460 | |
| 427 | - // 经典蓝牙:classic、dual、unknown(部分设备如 D320FAX 可能误报为 unknown,也尝试经典连接) | |
| 428 | - const useClassic = dev.type === 'classic' || dev.type === 'dual' || dev.type === 'unknown' | |
| 429 | - | |
| 430 | - if (useClassic) { | |
| 431 | - // #ifdef APP-PLUS | |
| 432 | - const classic = getClassicBluetooth() | |
| 433 | - if (classic && classic.connDevice) { | |
| 434 | - classic.connDevice(dev.deviceId, (ok: boolean) => { | |
| 435 | - connectingId.value = '' | |
| 436 | - if (ok) { | |
| 437 | - setBluetoothConnection({ | |
| 438 | - deviceId: dev.deviceId, | |
| 439 | - deviceName: dev.name, | |
| 440 | - deviceType: 'classic', | |
| 441 | - }) | |
| 442 | - uni.showToast({ title: 'Connected!', icon: 'success' }) | |
| 443 | - } else { | |
| 444 | - errorMsg.value = 'Connection failed. For D320FAX, ensure Virtual BT Printer is paired in system Bluetooth.' | |
| 445 | - } | |
| 446 | - }) | |
| 447 | - } else { | |
| 448 | - connectingId.value = '' | |
| 449 | - errorMsg.value = 'Classic Bluetooth not available. Ensure app is running on the device (not simulator).' | |
| 450 | - } | |
| 451 | - // #endif | |
| 452 | - // #ifndef APP-PLUS | |
| 461 | + const permissionResult = await ensureBluetoothPermissions({ connect: true }) | |
| 462 | + if (!permissionResult.ok) { | |
| 453 | 463 | connectingId.value = '' |
| 454 | - errorMsg.value = 'Classic Bluetooth requires the app.' | |
| 455 | - // #endif | |
| 464 | + errorMsg.value = permissionResult.message || 'Bluetooth permission denied.' | |
| 456 | 465 | return |
| 457 | 466 | } |
| 458 | 467 | |
| 459 | - uni.createBLEConnection({ | |
| 460 | - deviceId: dev.deviceId, | |
| 461 | - timeout: 10000, | |
| 462 | - success: async () => { | |
| 463 | - try { | |
| 464 | - const write = await findWriteCharacteristic(dev.deviceId) | |
| 465 | - if (!write) { | |
| 466 | - errorMsg.value = 'No writable characteristic found. This device may not support printing.' | |
| 467 | - connectingId.value = '' | |
| 468 | - return | |
| 469 | - } | |
| 470 | - setBluetoothConnection({ | |
| 471 | - deviceId: dev.deviceId, | |
| 472 | - deviceName: dev.name, | |
| 473 | - serviceId: write.serviceId, | |
| 474 | - characteristicId: write.characteristicId, | |
| 475 | - deviceType: 'ble', | |
| 476 | - mtu: 20, | |
| 477 | - }) | |
| 478 | - connectingId.value = '' | |
| 479 | - uni.showToast({ title: 'Connected!', icon: 'success' }) | |
| 480 | - } catch (e: any) { | |
| 481 | - errorMsg.value = (e && e.message) ? e.message : 'Connection failed' | |
| 482 | - connectingId.value = '' | |
| 483 | - } | |
| 484 | - }, | |
| 485 | - fail: (err: any) => { | |
| 486 | - connectingId.value = '' | |
| 487 | - if (err.errCode === -1) { | |
| 488 | - uni.showToast({ title: 'Already connected', icon: 'success' }) | |
| 489 | - } else { | |
| 490 | - errorMsg.value = 'Connection failed: ' + (err.errMsg || 'Try again') | |
| 491 | - } | |
| 492 | - }, | |
| 493 | - }) | |
| 468 | + try { | |
| 469 | + await connectBluetoothPrinter(dev) | |
| 470 | + refreshCurrentPrinter() | |
| 471 | + connectingId.value = '' | |
| 472 | + uni.showToast({ title: 'Connected!', icon: 'success' }) | |
| 473 | + } catch (e: any) { | |
| 474 | + errorMsg.value = (e && e.message) ? e.message : 'Connection failed' | |
| 475 | + connectingId.value = '' | |
| 476 | + } | |
| 494 | 477 | } |
| 495 | 478 | |
| 496 | 479 | const handleUseBuiltin = () => { |
| 497 | - setBuiltinPrinter() | |
| 480 | + useBuiltinPrinter() | |
| 481 | + refreshCurrentPrinter() | |
| 498 | 482 | uni.showToast({ title: 'Using built-in printer', icon: 'success' }) |
| 499 | 483 | } |
| 500 | 484 | |
| ... | ... | @@ -503,8 +487,7 @@ const handleTestPrint = async () => { |
| 503 | 487 | if (testPrinting.value) return |
| 504 | 488 | testPrinting.value = true |
| 505 | 489 | try { |
| 506 | - const data = buildTestTscLabel() | |
| 507 | - await sendToPrinter(data, (p) => { | |
| 490 | + await testPrintCurrentPrinter((p) => { | |
| 508 | 491 | if (p < 100) uni.showLoading({ title: `Printing ${p}%`, mask: true }) |
| 509 | 492 | }) |
| 510 | 493 | uni.hideLoading() |
| ... | ... | @@ -531,30 +514,14 @@ const handleTestPrint = async () => { |
| 531 | 514 | } |
| 532 | 515 | } |
| 533 | 516 | |
| 534 | -const handleDisconnect = () => { | |
| 535 | - const type = getPrinterType() | |
| 536 | - const deviceId = uni.getStorageSync('btDeviceId') | |
| 537 | - const deviceType = uni.getStorageSync('btDeviceType') | |
| 538 | - if (type === 'bluetooth' && deviceType === 'classic') { | |
| 539 | - // #ifdef APP-PLUS | |
| 540 | - const classic = getClassicBluetooth() | |
| 541 | - if (classic && classic.disConnDevice) classic.disConnDevice() | |
| 542 | - // #endif | |
| 543 | - } | |
| 544 | - clearPrinter() | |
| 545 | - if (type === 'bluetooth' && deviceId && deviceType !== 'classic') { | |
| 546 | - uni.closeBLEConnection({ | |
| 547 | - deviceId, | |
| 548 | - complete: () => { | |
| 549 | - uni.showToast({ title: 'Disconnected', icon: 'none' }) | |
| 550 | - }, | |
| 551 | - }) | |
| 552 | - } else { | |
| 553 | - uni.showToast({ title: 'Disconnected', icon: 'none' }) | |
| 554 | - } | |
| 517 | +const handleDisconnect = async () => { | |
| 518 | + await disconnectCurrentPrinter() | |
| 519 | + refreshCurrentPrinter() | |
| 520 | + uni.showToast({ title: 'Disconnected', icon: 'none' }) | |
| 555 | 521 | } |
| 556 | 522 | |
| 557 | 523 | onMounted(() => { |
| 524 | + debugInfo.value.classicModuleReady = !!classicBluetooth | |
| 558 | 525 | uni.onBluetoothDeviceFound(onDeviceFound) |
| 559 | 526 | uni.onBluetoothAdapterStateChange((res: any) => { |
| 560 | 527 | if (!res.available) { |
| ... | ... | @@ -567,6 +534,7 @@ onMounted(() => { |
| 567 | 534 | } |
| 568 | 535 | }) |
| 569 | 536 | printerType.value = getPrinterType() || 'bluetooth' |
| 537 | + refreshCurrentPrinter() | |
| 570 | 538 | }) |
| 571 | 539 | |
| 572 | 540 | onUnmounted(() => { |
| ... | ... | @@ -901,6 +869,37 @@ onUnmounted(() => { |
| 901 | 869 | gap: 12rpx; |
| 902 | 870 | } |
| 903 | 871 | |
| 872 | +.debug-card { | |
| 873 | + background: #fff7ed; | |
| 874 | + border: 1rpx solid #fed7aa; | |
| 875 | + border-radius: 20rpx; | |
| 876 | + padding: 24rpx; | |
| 877 | + margin-bottom: 24rpx; | |
| 878 | + display: flex; | |
| 879 | + flex-direction: column; | |
| 880 | + gap: 8rpx; | |
| 881 | +} | |
| 882 | + | |
| 883 | +.debug-title { | |
| 884 | + font-size: 28rpx; | |
| 885 | + font-weight: 700; | |
| 886 | + color: #9a3412; | |
| 887 | +} | |
| 888 | + | |
| 889 | +.debug-item { | |
| 890 | + font-size: 22rpx; | |
| 891 | + color: #7c2d12; | |
| 892 | +} | |
| 893 | + | |
| 894 | +.debug-error { | |
| 895 | + color: #b91c1c; | |
| 896 | +} | |
| 897 | + | |
| 898 | +.debug-warn { | |
| 899 | + color: #92400e; | |
| 900 | + font-weight: 600; | |
| 901 | +} | |
| 902 | + | |
| 904 | 903 | .device-tag { |
| 905 | 904 | display: inline-block; |
| 906 | 905 | font-size: 20rpx; | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
| ... | ... | @@ -118,8 +118,7 @@ import SideMenu from '../../components/SideMenu.vue' |
| 118 | 118 | import LocationPicker from '../../components/LocationPicker.vue' |
| 119 | 119 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 120 | 120 | import { generateNextLabelId } from '../../utils/printLog' |
| 121 | -import { getPrinterType, getBluetoothConnection, sendToPrinter } from '../../utils/print/printerConnection' | |
| 122 | -import { buildTscLabel } from '../../utils/print/tscLabelBuilder' | |
| 121 | +import { getCurrentPrinterSummary, printLabelForCurrentPrinter } from '../../utils/print/manager/printerManager' | |
| 123 | 122 | import chickenLabelImg from '../../static/chicken-lable.png' |
| 124 | 123 | |
| 125 | 124 | const statusBarHeight = getStatusBarHeight() |
| ... | ... | @@ -161,16 +160,9 @@ const displayProductName = computed(() => { |
| 161 | 160 | }) |
| 162 | 161 | |
| 163 | 162 | onShow(() => { |
| 164 | - const type = getPrinterType() | |
| 165 | - const conn = type === 'bluetooth' ? getBluetoothConnection() : null | |
| 166 | - btConnected.value = (type === 'bluetooth' && conn) || type === 'builtin' | |
| 167 | - if (type === 'builtin') { | |
| 168 | - btDeviceName.value = 'Built-in Printer' | |
| 169 | - } else if (conn) { | |
| 170 | - btDeviceName.value = conn.deviceName || '' | |
| 171 | - } else { | |
| 172 | - btDeviceName.value = '' | |
| 173 | - } | |
| 163 | + const summary = getCurrentPrinterSummary() | |
| 164 | + btConnected.value = summary.type === 'bluetooth' || summary.type === 'builtin' | |
| 165 | + btDeviceName.value = summary.displayName || '' | |
| 174 | 166 | }) |
| 175 | 167 | |
| 176 | 168 | interface ProductData { |
| ... | ... | @@ -245,13 +237,12 @@ const handlePrint = async () => { |
| 245 | 237 | } |
| 246 | 238 | isPrinting.value = true |
| 247 | 239 | try { |
| 248 | - const data = buildTscLabel({ | |
| 240 | + await printLabelForCurrentPrinter({ | |
| 249 | 241 | productName: displayProductName.value, |
| 250 | 242 | labelId: labelId.value, |
| 251 | 243 | printQty: printQty.value, |
| 252 | 244 | extraLine: lastEdited.value, |
| 253 | - }) | |
| 254 | - await sendToPrinter(data, (percent) => { | |
| 245 | + }, (percent) => { | |
| 255 | 246 | if (percent >= 100) return |
| 256 | 247 | uni.showLoading({ title: `Printing ${percent}%`, mask: true }) |
| 257 | 248 | }) | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue
| ... | ... | @@ -21,6 +21,20 @@ |
| 21 | 21 | <text class="badge-text">{{ t('printers.connected') }}</text> |
| 22 | 22 | </view> |
| 23 | 23 | |
| 24 | + <view class="debug-card"> | |
| 25 | + <text class="debug-title">Debug Status</text> | |
| 26 | + <text class="debug-item">Current Mode: {{ debugInfo.currentMode }}</text> | |
| 27 | + <text class="debug-item">Classic Module: {{ debugInfo.classicModuleReady ? 'Ready' : 'Not Ready' }}</text> | |
| 28 | + <text class="debug-item">Paired Count: {{ debugInfo.pairedCount }}</text> | |
| 29 | + <text class="debug-item">Virtual BT Printer: {{ debugInfo.foundVirtualPrinter ? 'Found' : 'Not Found' }}</text> | |
| 30 | + <text class="debug-item">Classic Scan: {{ debugInfo.lastClassicEvent }}</text> | |
| 31 | + <text class="debug-item">BLE Scan: {{ debugInfo.lastBleEvent }}</text> | |
| 32 | + <text v-if="debugInfo.lastBleError" class="debug-item debug-error">{{ debugInfo.lastBleError }}</text> | |
| 33 | + <text v-if="debugInfo.locationServiceRequired" class="debug-item debug-warn"> | |
| 34 | + Android system Location service is OFF. Turn it on in device settings before BLE scan. | |
| 35 | + </text> | |
| 36 | + </view> | |
| 37 | + | |
| 24 | 38 | <!-- Connected Device --> |
| 25 | 39 | <view v-if="currentType" class="printer-card connected"> |
| 26 | 40 | <view class="printer-icon"><AppIcon name="printer" size="md" color="white" /></view> |
| ... | ... | @@ -105,35 +119,51 @@ import { ref, onMounted, onUnmounted } from 'vue' |
| 105 | 119 | import { useI18n } from 'vue-i18n' |
| 106 | 120 | import AppIcon from '../../components/AppIcon.vue' |
| 107 | 121 | import SideMenu from '../../components/SideMenu.vue' |
| 122 | +import classicBluetooth from '../../utils/print/bluetoothTool.js' | |
| 123 | +import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions' | |
| 108 | 124 | import { |
| 109 | - getPrinterType, | |
| 110 | - getBluetoothConnection, | |
| 111 | - setBluetoothConnection, | |
| 112 | - setBuiltinPrinter, | |
| 113 | - clearPrinter, | |
| 114 | - sendToPrinter, | |
| 115 | - isBuiltinConnected | |
| 116 | -} from '../../utils/print/printerConnection' | |
| 117 | -import { buildTestTscLabel } from '../../utils/print/tscLabelBuilder' | |
| 118 | - | |
| 119 | -// #ifdef APP-PLUS | |
| 120 | -const classicBluetooth = (require('../../utils/print/bluetoothTool.js') as any).default | |
| 121 | -// #endif | |
| 125 | + connectBluetoothPrinter, | |
| 126 | + describeDiscoveredPrinter, | |
| 127 | + disconnectCurrentPrinter, | |
| 128 | + getCurrentPrinterSummary, | |
| 129 | + testPrintCurrentPrinter, | |
| 130 | + useBuiltinPrinter, | |
| 131 | +} from '../../utils/print/manager/printerManager' | |
| 122 | 132 | |
| 123 | 133 | const { t } = useI18n() |
| 124 | 134 | const isMenuOpen = ref(false) |
| 125 | 135 | |
| 126 | -const currentType = ref(getPrinterType()) | |
| 127 | -const currentBt = ref(getBluetoothConnection()) | |
| 136 | +const currentType = ref<'' | 'bluetooth' | 'builtin'>('') | |
| 137 | +const currentBt = ref<any>(null) | |
| 128 | 138 | const isScanning = ref(false) |
| 129 | 139 | const devices = ref<any[]>([]) |
| 130 | 140 | const pairedDevices = ref<any[]>([]) |
| 131 | 141 | const isConnecting = ref(false) |
| 142 | +const debugInfo = ref({ | |
| 143 | + currentMode: 'none', | |
| 144 | + classicModuleReady: false, | |
| 145 | + pairedCount: 0, | |
| 146 | + foundVirtualPrinter: false, | |
| 147 | + lastClassicEvent: 'idle', | |
| 148 | + lastBleEvent: 'idle', | |
| 149 | + lastBleError: '', | |
| 150 | + locationServiceRequired: false, | |
| 151 | +}) | |
| 132 | 152 | let bleListenerRegistered = false |
| 133 | 153 | |
| 134 | 154 | const refreshStatus = () => { |
| 135 | - currentType.value = getPrinterType() | |
| 136 | - currentBt.value = getBluetoothConnection() | |
| 155 | + const summary = getCurrentPrinterSummary() | |
| 156 | + currentType.value = summary.type | |
| 157 | + debugInfo.value.currentMode = summary.type || 'none' | |
| 158 | + currentBt.value = summary.type === 'bluetooth' | |
| 159 | + ? { | |
| 160 | + deviceName: summary.displayName, | |
| 161 | + deviceId: summary.deviceId, | |
| 162 | + deviceType: summary.deviceType, | |
| 163 | + driverName: summary.driverName, | |
| 164 | + protocol: summary.protocol, | |
| 165 | + } | |
| 166 | + : null | |
| 137 | 167 | } |
| 138 | 168 | |
| 139 | 169 | const getTypeLabel = (type?: string) => { |
| ... | ... | @@ -145,64 +175,140 @@ const getTypeLabel = (type?: string) => { |
| 145 | 175 | } |
| 146 | 176 | } |
| 147 | 177 | |
| 178 | +const normalizeDeviceName = (device: any) => { | |
| 179 | + return (device?.localName || device?.name || '').trim() || 'Unknown Device' | |
| 180 | +} | |
| 181 | + | |
| 182 | +const hasPreferredClassicDevice = () => { | |
| 183 | + return pairedDevices.value.some((item: any) => { | |
| 184 | + const name = String(item?.name || '').toLowerCase() | |
| 185 | + const type = String(item?.type || '').toLowerCase() | |
| 186 | + return name.includes('virtual bt printer') || type === 'classic' || type === 'dual' | |
| 187 | + }) | |
| 188 | +} | |
| 189 | + | |
| 148 | 190 | const addDeviceDedup = (device: any) => { |
| 191 | + const described = describeDiscoveredPrinter(device) | |
| 149 | 192 | const existing = devices.value.find(d => d.deviceId === device.deviceId) |
| 150 | 193 | if (!existing) { |
| 151 | - devices.value.push(device) | |
| 194 | + devices.value.push(described) | |
| 195 | + return | |
| 152 | 196 | } |
| 197 | + Object.assign(existing, described) | |
| 153 | 198 | } |
| 154 | 199 | |
| 155 | 200 | // #ifdef APP-PLUS |
| 156 | 201 | const loadPairedDevices = () => { |
| 157 | 202 | try { |
| 158 | 203 | const list = classicBluetooth.getPairedDevices() |
| 159 | - pairedDevices.value = list || [] | |
| 204 | + debugInfo.value.pairedCount = (list || []).length | |
| 205 | + debugInfo.value.foundVirtualPrinter = (list || []).some((item: any) => String(item?.name || '').toLowerCase().includes('virtual bt printer')) | |
| 206 | + pairedDevices.value = (list || []).map((item: any) => ({ | |
| 207 | + ...describeDiscoveredPrinter(item), | |
| 208 | + name: normalizeDeviceName(item), | |
| 209 | + })) | |
| 210 | + debugInfo.value.lastClassicEvent = pairedDevices.value.length > 0 ? 'paired devices loaded' : 'no paired devices' | |
| 160 | 211 | } catch (e) { |
| 161 | 212 | console.error('Failed to load paired devices', e) |
| 162 | 213 | pairedDevices.value = [] |
| 214 | + debugInfo.value.pairedCount = 0 | |
| 215 | + debugInfo.value.foundVirtualPrinter = false | |
| 216 | + debugInfo.value.lastClassicEvent = 'load paired devices failed' | |
| 163 | 217 | } |
| 164 | 218 | } |
| 165 | 219 | // #endif |
| 166 | 220 | |
| 167 | -const startScan = () => { | |
| 221 | +function mergeCachedBleDevices () { | |
| 222 | + uni.getBluetoothDevices({ | |
| 223 | + success: (res: any) => { | |
| 224 | + const list = res.devices || [] | |
| 225 | + list.forEach((device: any) => { | |
| 226 | + addDeviceDedup({ | |
| 227 | + ...device, | |
| 228 | + name: normalizeDeviceName(device), | |
| 229 | + type: device.type || 'ble', | |
| 230 | + }) | |
| 231 | + }) | |
| 232 | + }, | |
| 233 | + }) | |
| 234 | +} | |
| 235 | + | |
| 236 | +const startScan = async () => { | |
| 168 | 237 | if (isScanning.value) return |
| 169 | 238 | |
| 170 | 239 | devices.value = [] |
| 171 | - isScanning.value = true | |
| 172 | - | |
| 173 | - // #ifdef APP-PLUS | |
| 174 | - loadPairedDevices() | |
| 175 | - try { | |
| 176 | - classicBluetooth.startClassicDiscovery( | |
| 177 | - (device: any) => { | |
| 178 | - if (device.name) { | |
| 179 | - addDeviceDedup(device) | |
| 180 | - } | |
| 181 | - }, | |
| 182 | - () => {} | |
| 183 | - ) | |
| 184 | - } catch (e) { | |
| 185 | - console.error('Classic discovery failed', e) | |
| 240 | + debugInfo.value.lastBleError = '' | |
| 241 | + debugInfo.value.locationServiceRequired = false | |
| 242 | + debugInfo.value.lastClassicEvent = 'starting' | |
| 243 | + debugInfo.value.lastBleEvent = 'starting' | |
| 244 | + | |
| 245 | + const permissionResult = await ensureBluetoothPermissions({ scan: true, connect: true }) | |
| 246 | + if (!permissionResult.ok) { | |
| 247 | + uni.showToast({ title: permissionResult.message || t('printers.bleNotAvailable'), icon: 'none' }) | |
| 248 | + return | |
| 186 | 249 | } |
| 187 | - // #endif | |
| 188 | 250 | |
| 189 | 251 | uni.openBluetoothAdapter({ |
| 190 | 252 | success: () => { |
| 253 | + // #ifdef APP-PLUS | |
| 254 | + loadPairedDevices() | |
| 255 | + try { | |
| 256 | + classicBluetooth.startClassicDiscovery( | |
| 257 | + (device: any) => { | |
| 258 | + debugInfo.value.lastClassicEvent = 'device found' | |
| 259 | + addDeviceDedup({ | |
| 260 | + ...device, | |
| 261 | + name: normalizeDeviceName(device), | |
| 262 | + }) | |
| 263 | + }, | |
| 264 | + () => { | |
| 265 | + debugInfo.value.lastClassicEvent = 'scan finished' | |
| 266 | + } | |
| 267 | + ) | |
| 268 | + debugInfo.value.lastClassicEvent = 'scan running' | |
| 269 | + } catch (e) { | |
| 270 | + console.error('Classic discovery failed', e) | |
| 271 | + debugInfo.value.lastClassicEvent = 'scan failed' | |
| 272 | + } | |
| 273 | + // #endif | |
| 274 | + isScanning.value = true | |
| 275 | + | |
| 276 | + if (hasPreferredClassicDevice()) { | |
| 277 | + debugInfo.value.lastBleEvent = 'skipped (paired classic device found)' | |
| 278 | + return | |
| 279 | + } | |
| 280 | + | |
| 281 | + mergeCachedBleDevices() | |
| 191 | 282 | if (!bleListenerRegistered) { |
| 192 | 283 | bleListenerRegistered = true |
| 193 | 284 | uni.onBluetoothDeviceFound((res) => { |
| 194 | 285 | res.devices.forEach(device => { |
| 195 | - if (device.name) { | |
| 196 | - addDeviceDedup({ ...device, type: 'ble' }) | |
| 197 | - } | |
| 286 | + addDeviceDedup({ | |
| 287 | + ...device, | |
| 288 | + name: normalizeDeviceName(device), | |
| 289 | + type: 'ble', | |
| 290 | + }) | |
| 198 | 291 | }) |
| 199 | 292 | }) |
| 200 | 293 | } |
| 201 | 294 | uni.startBluetoothDevicesDiscovery({ |
| 202 | 295 | allowDuplicatesKey: false, |
| 203 | - success: () => {}, | |
| 296 | + success: () => { | |
| 297 | + debugInfo.value.lastBleEvent = 'scan running' | |
| 298 | + }, | |
| 204 | 299 | fail: (err) => { |
| 205 | 300 | console.error('BLE startBluetoothDevicesDiscovery fail:', err) |
| 301 | + debugInfo.value.lastBleEvent = 'scan failed' | |
| 302 | + debugInfo.value.lastBleError = err?.errMsg || 'BLE scan failed' | |
| 303 | + if (err?.errCode === 10016 || err?.code === 10016) { | |
| 304 | + debugInfo.value.locationServiceRequired = true | |
| 305 | + debugInfo.value.lastBleError = 'BLE scan failed: system Location service is turned off.' | |
| 306 | + uni.showToast({ | |
| 307 | + title: 'Turn on system Location service first', | |
| 308 | + icon: 'none', | |
| 309 | + duration: 2500, | |
| 310 | + }) | |
| 311 | + } | |
| 206 | 312 | } |
| 207 | 313 | }) |
| 208 | 314 | }, |
| ... | ... | @@ -217,6 +323,8 @@ const startScan = () => { |
| 217 | 323 | |
| 218 | 324 | const stopScan = () => { |
| 219 | 325 | isScanning.value = false |
| 326 | + debugInfo.value.lastClassicEvent = 'stopped' | |
| 327 | + debugInfo.value.lastBleEvent = 'stopped' | |
| 220 | 328 | uni.stopBluetoothDevicesDiscovery({ |
| 221 | 329 | success: () => console.log('Stop BLE scan success'), |
| 222 | 330 | fail: (err) => console.error('Stop BLE scan fail', err) |
| ... | ... | @@ -230,140 +338,56 @@ const stopScan = () => { |
| 230 | 338 | // #endif |
| 231 | 339 | } |
| 232 | 340 | |
| 233 | -const connectBt = (device: any) => { | |
| 341 | +const connectBt = async (device: any) => { | |
| 234 | 342 | if (isConnecting.value) return |
| 235 | 343 | isConnecting.value = true |
| 236 | 344 | stopScan() |
| 237 | 345 | |
| 238 | - uni.showLoading({ title: t('printers.connecting') }) | |
| 239 | - | |
| 240 | - const deviceType = device.type || 'ble' | |
| 241 | - | |
| 242 | - if (deviceType === 'classic' || deviceType === 'dual') { | |
| 243 | - // #ifdef APP-PLUS | |
| 244 | - classicBluetooth.connDevice(device.deviceId, (success: boolean) => { | |
| 245 | - isConnecting.value = false | |
| 246 | - uni.hideLoading() | |
| 247 | - if (success) { | |
| 248 | - setBluetoothConnection({ | |
| 249 | - deviceId: device.deviceId, | |
| 250 | - deviceName: device.name || 'Bluetooth Printer', | |
| 251 | - deviceType: 'classic' | |
| 252 | - }) | |
| 253 | - currentType.value = 'bluetooth' | |
| 254 | - currentBt.value = getBluetoothConnection() | |
| 255 | - uni.showToast({ title: t('printers.connectSuccess') }) | |
| 256 | - } else { | |
| 257 | - uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) | |
| 258 | - } | |
| 259 | - }) | |
| 260 | - // #endif | |
| 261 | - // #ifndef APP-PLUS | |
| 346 | + const permissionResult = await ensureBluetoothPermissions({ connect: true }) | |
| 347 | + if (!permissionResult.ok) { | |
| 262 | 348 | isConnecting.value = false |
| 263 | - uni.hideLoading() | |
| 264 | - uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) | |
| 265 | - // #endif | |
| 266 | - } else { | |
| 267 | - connectBle(device) | |
| 349 | + uni.showToast({ title: permissionResult.message || t('printers.connectFail'), icon: 'none' }) | |
| 350 | + return | |
| 268 | 351 | } |
| 269 | -} | |
| 270 | - | |
| 271 | -const connectBle = (device: any) => { | |
| 272 | - uni.createBLEConnection({ | |
| 273 | - deviceId: device.deviceId, | |
| 274 | - success: () => { | |
| 275 | - uni.getBLEDeviceServices({ | |
| 276 | - deviceId: device.deviceId, | |
| 277 | - success: (res) => { | |
| 278 | - findWriteCharacteristic(device.deviceId, res.services, device.name) | |
| 279 | - }, | |
| 280 | - fail: () => { | |
| 281 | - isConnecting.value = false | |
| 282 | - uni.hideLoading() | |
| 283 | - uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) | |
| 284 | - } | |
| 285 | - }) | |
| 286 | - }, | |
| 287 | - fail: () => { | |
| 288 | - isConnecting.value = false | |
| 289 | - uni.hideLoading() | |
| 290 | - uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) | |
| 291 | - } | |
| 292 | - }) | |
| 293 | -} | |
| 294 | 352 | |
| 295 | -const findWriteCharacteristic = (deviceId: string, services: any[], deviceName: string) => { | |
| 296 | - let found = false | |
| 297 | - let serviceIdx = 0 | |
| 298 | - | |
| 299 | - const nextService = () => { | |
| 300 | - if (serviceIdx >= services.length || found) { | |
| 301 | - if (!found) { | |
| 302 | - isConnecting.value = false | |
| 303 | - uni.hideLoading() | |
| 304 | - uni.showToast({ title: 'No write characteristic found', icon: 'none' }) | |
| 305 | - } | |
| 306 | - return | |
| 307 | - } | |
| 308 | - | |
| 309 | - const service = services[serviceIdx++] | |
| 310 | - uni.getBLEDeviceCharacteristics({ | |
| 311 | - deviceId, | |
| 312 | - serviceId: service.uuid, | |
| 313 | - success: (res) => { | |
| 314 | - const char = res.characteristics.find(c => c.properties.write) | |
| 315 | - if (char) { | |
| 316 | - found = true | |
| 317 | - setBluetoothConnection({ | |
| 318 | - deviceId, | |
| 319 | - deviceName, | |
| 320 | - serviceId: service.uuid, | |
| 321 | - characteristicId: char.uuid, | |
| 322 | - deviceType: 'ble' | |
| 323 | - }) | |
| 324 | - currentType.value = 'bluetooth' | |
| 325 | - currentBt.value = getBluetoothConnection() | |
| 326 | - isConnecting.value = false | |
| 327 | - uni.hideLoading() | |
| 328 | - uni.showToast({ title: t('printers.connectSuccess') }) | |
| 329 | - } else { | |
| 330 | - nextService() | |
| 331 | - } | |
| 332 | - }, | |
| 333 | - fail: () => nextService() | |
| 334 | - }) | |
| 353 | + uni.showLoading({ title: t('printers.connecting') }) | |
| 354 | + try { | |
| 355 | + await connectBluetoothPrinter(device) | |
| 356 | + refreshStatus() | |
| 357 | + uni.hideLoading() | |
| 358 | + uni.showToast({ title: t('printers.connectSuccess') }) | |
| 359 | + } catch (e) { | |
| 360 | + uni.hideLoading() | |
| 361 | + uni.showToast({ title: t('printers.connectFail'), icon: 'none' }) | |
| 362 | + } finally { | |
| 363 | + isConnecting.value = false | |
| 335 | 364 | } |
| 336 | - | |
| 337 | - nextService() | |
| 338 | 365 | } |
| 339 | 366 | |
| 340 | 367 | const connectBuiltin = () => { |
| 341 | - setBuiltinPrinter() | |
| 342 | - currentType.value = 'builtin' | |
| 368 | + useBuiltinPrinter() | |
| 369 | + refreshStatus() | |
| 343 | 370 | uni.showToast({ title: t('printers.connectSuccess') }) |
| 344 | 371 | } |
| 345 | 372 | |
| 346 | -const disconnect = () => { | |
| 347 | - // #ifdef APP-PLUS | |
| 348 | - if (currentBt.value?.deviceType === 'classic') { | |
| 349 | - try { | |
| 350 | - classicBluetooth.disConnDevice() | |
| 351 | - } catch (e) { | |
| 352 | - console.error('Disconnect classic bluetooth failed', e) | |
| 353 | - } | |
| 354 | - } | |
| 355 | - // #endif | |
| 356 | - clearPrinter() | |
| 357 | - currentType.value = '' | |
| 358 | - currentBt.value = null | |
| 373 | +const disconnect = async () => { | |
| 374 | + await disconnectCurrentPrinter() | |
| 375 | + refreshStatus() | |
| 359 | 376 | uni.showToast({ title: t('printers.disconnected') }) |
| 360 | 377 | } |
| 361 | 378 | |
| 362 | 379 | const doTestPrint = async () => { |
| 363 | 380 | try { |
| 364 | - const data = buildTestTscLabel() | |
| 381 | + if (currentType.value === 'builtin') { | |
| 382 | + uni.showModal({ | |
| 383 | + title: 'Current Mode is Built-in', | |
| 384 | + 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.', | |
| 385 | + showCancel: false, | |
| 386 | + }) | |
| 387 | + return | |
| 388 | + } | |
| 365 | 389 | uni.showLoading({ title: t('labels.print.printing') }) |
| 366 | - await sendToPrinter(data) | |
| 390 | + await testPrintCurrentPrinter() | |
| 367 | 391 | uni.hideLoading() |
| 368 | 392 | uni.showToast({ title: t('printers.testPrintSuccess') }) |
| 369 | 393 | } catch (e: any) { |
| ... | ... | @@ -393,6 +417,7 @@ const goBack = () => { |
| 393 | 417 | |
| 394 | 418 | onMounted(() => { |
| 395 | 419 | refreshStatus() |
| 420 | + debugInfo.value.classicModuleReady = !!classicBluetooth | |
| 396 | 421 | // #ifdef APP-PLUS |
| 397 | 422 | loadPairedDevices() |
| 398 | 423 | // #endif |
| ... | ... | @@ -428,6 +453,11 @@ onUnmounted(() => { |
| 428 | 453 | .info-badge { display: flex; align-items: center; gap: 12rpx; margin-bottom: 32rpx; } |
| 429 | 454 | .badge-num { font-size: 40rpx; font-weight: 700; color: var(--theme-primary); } |
| 430 | 455 | .badge-text { font-size: 28rpx; color: #6b7280; } |
| 456 | +.debug-card { background: #fff7ed; border: 1rpx solid #fed7aa; border-radius: 20rpx; padding: 24rpx; margin-bottom: 24rpx; display: flex; flex-direction: column; gap: 8rpx; } | |
| 457 | +.debug-title { font-size: 28rpx; font-weight: 700; color: #9a3412; } | |
| 458 | +.debug-item { font-size: 22rpx; color: #7c2d12; } | |
| 459 | +.debug-error { color: #b91c1c; } | |
| 460 | +.debug-warn { color: #92400e; font-weight: 600; } | |
| 431 | 461 | .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; } |
| 432 | 462 | .printer-card.connected { background: var(--theme-primary); border: none; } |
| 433 | 463 | .printer-card.connected .printer-name, .printer-card.connected .printer-loc, .printer-card.connected .printer-status { color: #fff; } | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothPermissions.ts
0 → 100644
| 1 | +export interface BluetoothPermissionResult { | |
| 2 | + ok: boolean | |
| 3 | + message?: string | |
| 4 | +} | |
| 5 | + | |
| 6 | +function normalizePermissionName (permission: string): string { | |
| 7 | + return String(permission || '').split('.').pop() || String(permission || '') | |
| 8 | +} | |
| 9 | + | |
| 10 | +function formatDeniedMessage (permissions: string[]): string { | |
| 11 | + if (!permissions.length) return 'Bluetooth permission denied.' | |
| 12 | + return 'Please allow Bluetooth permissions: ' + permissions.map(normalizePermissionName).join(', ') | |
| 13 | +} | |
| 14 | + | |
| 15 | +function requestAndroidPermissions (permissions: string[]): Promise<BluetoothPermissionResult> { | |
| 16 | + return new Promise((resolve) => { | |
| 17 | + // #ifdef APP-PLUS | |
| 18 | + try { | |
| 19 | + if (typeof plus === 'undefined' || !plus.android || !permissions.length) { | |
| 20 | + resolve({ ok: true }) | |
| 21 | + return | |
| 22 | + } | |
| 23 | + plus.android.requestPermissions( | |
| 24 | + permissions, | |
| 25 | + (resultObj: any) => { | |
| 26 | + const deniedPresent = Array.isArray(resultObj?.deniedPresent) ? resultObj.deniedPresent : [] | |
| 27 | + const deniedAlways = Array.isArray(resultObj?.deniedAlways) ? resultObj.deniedAlways : [] | |
| 28 | + const denied = [...deniedPresent, ...deniedAlways] | |
| 29 | + if (denied.length > 0) { | |
| 30 | + resolve({ | |
| 31 | + ok: false, | |
| 32 | + message: formatDeniedMessage(denied), | |
| 33 | + }) | |
| 34 | + return | |
| 35 | + } | |
| 36 | + resolve({ ok: true }) | |
| 37 | + }, | |
| 38 | + () => { | |
| 39 | + resolve({ | |
| 40 | + ok: false, | |
| 41 | + message: 'Failed to request Bluetooth permissions.', | |
| 42 | + }) | |
| 43 | + } | |
| 44 | + ) | |
| 45 | + return | |
| 46 | + } catch (e: any) { | |
| 47 | + resolve({ | |
| 48 | + ok: false, | |
| 49 | + message: e?.message || 'Failed to request Bluetooth permissions.', | |
| 50 | + }) | |
| 51 | + return | |
| 52 | + } | |
| 53 | + // #endif | |
| 54 | + resolve({ ok: true }) | |
| 55 | + }) | |
| 56 | +} | |
| 57 | + | |
| 58 | +export async function ensureBluetoothPermissions (options?: { | |
| 59 | + scan?: boolean | |
| 60 | + connect?: boolean | |
| 61 | +}): Promise<BluetoothPermissionResult> { | |
| 62 | + const { scan = false, connect = false } = options || {} | |
| 63 | + | |
| 64 | + // #ifdef APP-PLUS | |
| 65 | + try { | |
| 66 | + if (typeof plus === 'undefined' || !plus.android) { | |
| 67 | + return { ok: true } | |
| 68 | + } | |
| 69 | + const Build = plus.android.importClass('android.os.Build') | |
| 70 | + const sdkInt = Number(Build.VERSION.SDK_INT || 0) | |
| 71 | + | |
| 72 | + const permissions = new Set<string>() | |
| 73 | + if (sdkInt >= 31) { | |
| 74 | + if (scan) permissions.add('android.permission.BLUETOOTH_SCAN') | |
| 75 | + if (scan || connect) permissions.add('android.permission.BLUETOOTH_CONNECT') | |
| 76 | + if (scan) permissions.add('android.permission.ACCESS_FINE_LOCATION') | |
| 77 | + } else if (scan) { | |
| 78 | + permissions.add('android.permission.ACCESS_FINE_LOCATION') | |
| 79 | + permissions.add('android.permission.ACCESS_COARSE_LOCATION') | |
| 80 | + } | |
| 81 | + | |
| 82 | + return await requestAndroidPermissions(Array.from(permissions)) | |
| 83 | + } catch (e: any) { | |
| 84 | + return { | |
| 85 | + ok: false, | |
| 86 | + message: e?.message || 'Bluetooth permission check failed.', | |
| 87 | + } | |
| 88 | + } | |
| 89 | + // #endif | |
| 90 | + | |
| 91 | + return { ok: true } | |
| 92 | +} | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/bluetoothTool.js
| 1 | 1 | /** |
| 2 | - * 经典蓝牙工具(仅 Android APP-PLUS) | |
| 3 | - * 用于佳博 D320FAX 等一体机的 Virtual BT Printer 连接 | |
| 4 | - * 支持 RFCOMM 回退连接(部分设备需要) | |
| 2 | + * 经典蓝牙工具(Android APP-PLUS) | |
| 3 | + * 兼容官方 UniApp SDK 经典蓝牙实现,并补充当前项目需要的简化调用: | |
| 4 | + * - getPairedDevices() | |
| 5 | + * - startClassicDiscovery(onDeviceFound, onDiscoveryFinished) | |
| 6 | + * - cancelClassicDiscovery() | |
| 7 | + * - connDevice(address, callback) | |
| 8 | + * - disConnDevice() | |
| 9 | + * - sendByteData(byteData) | |
| 5 | 10 | */ |
| 11 | + | |
| 6 | 12 | // #ifdef APP-PLUS |
| 7 | -function getAdapter () { | |
| 8 | - if (typeof plus === 'undefined' || !plus.android) return null | |
| 9 | - const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter') | |
| 10 | - return BluetoothAdapter.getDefaultAdapter() | |
| 11 | -} | |
| 13 | +let BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter') | |
| 14 | +let Intent = plus.android.importClass('android.content.Intent') | |
| 15 | +let IntentFilter = plus.android.importClass('android.content.IntentFilter') | |
| 16 | +let BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice') | |
| 17 | +let UUID = plus.android.importClass('java.util.UUID') | |
| 18 | +let Toast = plus.android.importClass('android.widget.Toast') | |
| 19 | +let MY_UUID = UUID.fromString('00001101-0000-1000-8000-00805F9B34FB') | |
| 20 | + | |
| 21 | +let invoke = plus.android.invoke | |
| 22 | +let btAdapter = BluetoothAdapter.getDefaultAdapter() | |
| 23 | +let activity = plus.android.runtimeMainActivity() | |
| 24 | + | |
| 25 | +let btSocket = null | |
| 26 | +let btInStream = null | |
| 27 | +let btOutStream = null | |
| 28 | +let setIntervalId = 0 | |
| 29 | + | |
| 30 | +let btFindReceiver = null | |
| 31 | +let btStatusReceiver = null | |
| 32 | +// #endif | |
| 12 | 33 | |
| 13 | -function getActivity () { | |
| 14 | - return typeof plus !== 'undefined' ? plus.android.runtimeMainActivity() : null | |
| 34 | +function normalizeDeviceType (deviceType) { | |
| 35 | + if (deviceType === 1) return 'classic' | |
| 36 | + if (deviceType === 2) return 'ble' | |
| 37 | + if (deviceType === 3) return 'dual' | |
| 38 | + return 'unknown' | |
| 15 | 39 | } |
| 16 | 40 | |
| 17 | -function showToast (msg) { | |
| 41 | +function fallbackRfcommSocket (device, insecure = false) { | |
| 18 | 42 | try { |
| 19 | - const activity = getActivity() | |
| 20 | - if (activity) { | |
| 21 | - const Toast = plus.android.importClass('android.widget.Toast') | |
| 22 | - Toast.makeText(activity, String(msg), Toast.LENGTH_SHORT).show() | |
| 23 | - } | |
| 24 | - } catch (_) {} | |
| 43 | + const cls = invoke(device, 'getClass') | |
| 44 | + const intClass = plus.android.importClass('java.lang.Integer').TYPE | |
| 45 | + const methodName = insecure ? 'createInsecureRfcommSocket' : 'createRfcommSocket' | |
| 46 | + const method = invoke(cls, 'getMethod', methodName, intClass) | |
| 47 | + return invoke(method, 'invoke', device, 1) | |
| 48 | + } catch (e) { | |
| 49 | + console.error('RFCOMM fallback failed:', e) | |
| 50 | + return null | |
| 51 | + } | |
| 25 | 52 | } |
| 26 | 53 | |
| 27 | -function getInvoke () { | |
| 28 | - return typeof plus !== 'undefined' && plus.android ? plus.android.invoke : null | |
| 54 | +function getErrorMessage (error) { | |
| 55 | + if (!error) return 'Unknown error' | |
| 56 | + if (typeof error === 'string') return error | |
| 57 | + return String(error.message || error.errMsg || error) | |
| 29 | 58 | } |
| 30 | -function getMyUuid () { | |
| 31 | - if (typeof plus === 'undefined' || !plus.android) return null | |
| 32 | - return plus.android.importClass('java.util.UUID').fromString('00001101-0000-1000-8000-00805F9B34FB') | |
| 59 | + | |
| 60 | +function normalizeWriteByte (value) { | |
| 61 | + const byte = Number(value) || 0 | |
| 62 | + return byte & 0xff | |
| 63 | +} | |
| 64 | + | |
| 65 | +function normalizeWriteChunk (byteData, start, end) { | |
| 66 | + const out = [] | |
| 67 | + for (let i = start; i < end; i++) { | |
| 68 | + const value = normalizeWriteByte(byteData[i]) | |
| 69 | + if (value >= 128) { | |
| 70 | + out.push(value - 256) | |
| 71 | + } else { | |
| 72 | + out.push(value) | |
| 73 | + } | |
| 74 | + } | |
| 75 | + return out | |
| 76 | +} | |
| 77 | + | |
| 78 | +function createSocketCandidates (device) { | |
| 79 | + return [ | |
| 80 | + { | |
| 81 | + name: 'secure-service-record', | |
| 82 | + create: () => invoke(device, 'createRfcommSocketToServiceRecord', MY_UUID), | |
| 83 | + }, | |
| 84 | + { | |
| 85 | + name: 'insecure-service-record', | |
| 86 | + create: () => invoke(device, 'createInsecureRfcommSocketToServiceRecord', MY_UUID), | |
| 87 | + }, | |
| 88 | + { | |
| 89 | + name: 'secure-channel-1', | |
| 90 | + create: () => fallbackRfcommSocket(device, false), | |
| 91 | + }, | |
| 92 | + { | |
| 93 | + name: 'insecure-channel-1', | |
| 94 | + create: () => fallbackRfcommSocket(device, true), | |
| 95 | + }, | |
| 96 | + ] | |
| 33 | 97 | } |
| 34 | -let btSocket = null | |
| 35 | -let btInStream = null | |
| 36 | -let btOutStream = null | |
| 37 | -let btFindReceiver = null | |
| 38 | -// #endif | |
| 39 | 98 | |
| 40 | 99 | var blueToothTool = { |
| 41 | - state: { bluetoothEnable: false, readThreadState: false }, | |
| 100 | + state: { | |
| 101 | + bluetoothEnable: false, | |
| 102 | + bluetoothState: '', | |
| 103 | + discoveryDeviceState: false, | |
| 104 | + readThreadState: false, | |
| 105 | + connectionState: 'idle', | |
| 106 | + lastAddress: '', | |
| 107 | + lastSocketStrategy: '', | |
| 108 | + lastError: '', | |
| 109 | + lastSendError: '', | |
| 110 | + outputReady: false, | |
| 111 | + lastSendMode: 'idle', | |
| 112 | + }, | |
| 113 | + options: { | |
| 114 | + listenBTStatusCallback: function (state) {}, | |
| 115 | + discoveryDeviceCallback: function (newDevice) {}, | |
| 116 | + discoveryFinishedCallback: function () {}, | |
| 117 | + readDataCallback: function (dataByteArr) {}, | |
| 118 | + connExceptionCallback: function (e) {}, | |
| 119 | + }, | |
| 120 | + init (setOptions) { | |
| 121 | + Object.assign(this.options, setOptions || {}) | |
| 122 | + this.state.bluetoothEnable = this.getBluetoothStatus() | |
| 123 | + this.listenBluetoothStatus() | |
| 124 | + }, | |
| 125 | + setErrorState (message, type = 'general') { | |
| 126 | + const text = String(message || '') | |
| 127 | + this.state.lastError = text | |
| 128 | + if (type === 'send') { | |
| 129 | + this.state.lastSendError = text | |
| 130 | + } | |
| 131 | + }, | |
| 132 | + clearErrorState () { | |
| 133 | + this.state.lastError = '' | |
| 134 | + this.state.lastSendError = '' | |
| 135 | + }, | |
| 42 | 136 | shortToast (msg) { |
| 43 | 137 | // #ifdef APP-PLUS |
| 44 | - showToast(msg) | |
| 138 | + try { | |
| 139 | + if (activity) Toast.makeText(activity, String(msg), Toast.LENGTH_SHORT).show() | |
| 140 | + } catch (_) {} | |
| 45 | 141 | // #endif |
| 46 | 142 | }, |
| 143 | + isSupportBluetooth () { | |
| 144 | + // #ifdef APP-PLUS | |
| 145 | + return btAdapter != null | |
| 146 | + // #endif | |
| 147 | + return false | |
| 148 | + }, | |
| 47 | 149 | getBluetoothStatus () { |
| 48 | 150 | // #ifdef APP-PLUS |
| 49 | - const btAdapter = getAdapter() | |
| 50 | 151 | return btAdapter != null && btAdapter.isEnabled() |
| 51 | 152 | // #endif |
| 52 | 153 | return false |
| 53 | 154 | }, |
| 54 | - /** 获取已配对设备(含 Virtual BT Printer / D320FAX) */ | |
| 155 | + turnOnBluetooth () { | |
| 156 | + // #ifdef APP-PLUS | |
| 157 | + if (btAdapter == null) { | |
| 158 | + this.shortToast('Bluetooth not available') | |
| 159 | + return | |
| 160 | + } | |
| 161 | + if (!btAdapter.isEnabled()) { | |
| 162 | + if (activity == null) { | |
| 163 | + this.shortToast('Activity not available') | |
| 164 | + return | |
| 165 | + } | |
| 166 | + let intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) | |
| 167 | + activity.startActivityForResult(intent, 1) | |
| 168 | + return | |
| 169 | + } | |
| 170 | + this.shortToast('Bluetooth already enabled') | |
| 171 | + // #endif | |
| 172 | + }, | |
| 173 | + turnOffBluetooth () { | |
| 174 | + // #ifdef APP-PLUS | |
| 175 | + if (btAdapter != null && btAdapter.isEnabled()) { | |
| 176 | + btAdapter.disable() | |
| 177 | + } | |
| 178 | + if (btFindReceiver != null) { | |
| 179 | + try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} | |
| 180 | + btFindReceiver = null | |
| 181 | + } | |
| 182 | + this.state.bluetoothEnable = false | |
| 183 | + this.cancelDiscovery() | |
| 184 | + this.closeBtSocket() | |
| 185 | + // #endif | |
| 186 | + }, | |
| 55 | 187 | getPairedDevices () { |
| 56 | 188 | // #ifdef APP-PLUS |
| 57 | - const pairedDevices = [] | |
| 189 | + let pairedDevices = [] | |
| 58 | 190 | try { |
| 59 | - const btAdapter = getAdapter() | |
| 60 | - if (!btAdapter || !btAdapter.isEnabled()) { | |
| 61 | - this.shortToast('Bluetooth is off') | |
| 62 | - return pairedDevices | |
| 63 | - } | |
| 64 | - const bonded = btAdapter.getBondedDevices() | |
| 65 | - if (!bonded) return pairedDevices | |
| 66 | - const inv = getInvoke() | |
| 67 | - if (!inv) return pairedDevices | |
| 68 | - const it = inv(bonded, 'iterator') | |
| 69 | - while (inv(it, 'hasNext')) { | |
| 70 | - const device = inv(it, 'next') | |
| 71 | - const deviceType = inv(device, 'getType') | |
| 72 | - const deviceId = inv(device, 'getAddress') | |
| 73 | - let deviceName = inv(device, 'getName') | |
| 74 | - if (deviceName == null) deviceName = '' | |
| 75 | - deviceName = String(deviceName).trim() || 'Unknown Device' | |
| 76 | - let typeStr = 'unknown' | |
| 77 | - if (deviceType === 1) typeStr = 'classic' | |
| 78 | - else if (deviceType === 2) typeStr = 'ble' | |
| 79 | - else if (deviceType === 3) typeStr = 'dual' | |
| 80 | - pairedDevices.push({ name: deviceName, deviceId: String(deviceId), type: typeStr }) | |
| 191 | + if (btAdapter == null || !btAdapter.isEnabled()) return pairedDevices | |
| 192 | + let pairedDevicesAndroid = btAdapter.getBondedDevices() | |
| 193 | + if (!pairedDevicesAndroid) return pairedDevices | |
| 194 | + let it = invoke(pairedDevicesAndroid, 'iterator') | |
| 195 | + while (invoke(it, 'hasNext')) { | |
| 196 | + let device = invoke(it, 'next') | |
| 197 | + let deviceType = invoke(device, 'getType') | |
| 198 | + let deviceId = invoke(device, 'getAddress') | |
| 199 | + let deviceName = invoke(device, 'getName') | |
| 200 | + pairedDevices.push({ | |
| 201 | + name: deviceName != null ? String(deviceName).trim() || 'Unknown Device' : 'Unknown Device', | |
| 202 | + deviceId: String(deviceId || ''), | |
| 203 | + type: normalizeDeviceType(deviceType), | |
| 204 | + }) | |
| 81 | 205 | } |
| 82 | 206 | } catch (e) { |
| 83 | 207 | console.error('getPairedDevices error:', e) |
| ... | ... | @@ -86,156 +210,360 @@ var blueToothTool = { |
| 86 | 210 | // #endif |
| 87 | 211 | return [] |
| 88 | 212 | }, |
| 89 | - /** 经典蓝牙扫描(发现未配对设备,如 d320fax_295c)— 不过滤任何设备 */ | |
| 90 | - startClassicDiscovery (onDeviceFound, onDiscoveryFinished) { | |
| 213 | + discoveryNewDevice () { | |
| 91 | 214 | // #ifdef APP-PLUS |
| 92 | - const btAdapter = getAdapter() | |
| 93 | - const activity = getActivity() | |
| 94 | - if (!btAdapter || !btAdapter.isEnabled() || !activity) return | |
| 95 | - if (btFindReceiver) { | |
| 96 | - try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} | |
| 215 | + if (btAdapter == null || !btAdapter.isEnabled() || activity == null) return | |
| 216 | + if (btFindReceiver != null) { | |
| 217 | + try { activity.unregisterReceiver(btFindReceiver) } catch (e) { console.error(e) } | |
| 97 | 218 | btFindReceiver = null |
| 219 | + this.cancelDiscovery() | |
| 98 | 220 | } |
| 99 | - if (btAdapter.isDiscovering()) btAdapter.cancelDiscovery() | |
| 100 | - const BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter') | |
| 101 | - const BluetoothDevice = plus.android.importClass('android.bluetooth.BluetoothDevice') | |
| 102 | - const IntentFilter = plus.android.importClass('android.content.IntentFilter') | |
| 103 | - const inv = getInvoke() | |
| 221 | + let options = this.options | |
| 104 | 222 | btFindReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', { |
| 105 | - onReceive (context, intent) { | |
| 106 | - const action = intent.getAction() | |
| 223 | + onReceive: function (context, intent) { | |
| 224 | + plus.android.importClass(context) | |
| 225 | + plus.android.importClass(intent) | |
| 226 | + let action = intent.getAction() | |
| 107 | 227 | if (BluetoothDevice.ACTION_FOUND === action) { |
| 108 | - const device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) | |
| 228 | + let device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) | |
| 109 | 229 | if (!device) return |
| 110 | - const deviceType = inv(device, 'getType') | |
| 111 | - const deviceId = String(inv(device, 'getAddress') || '') | |
| 112 | - let deviceName = inv(device, 'getName') | |
| 113 | - if (deviceName == null) deviceName = '' | |
| 114 | - deviceName = String(deviceName).trim() || 'Unknown Device' | |
| 115 | - let typeStr = 'unknown' | |
| 116 | - if (deviceType === 1) typeStr = 'classic' | |
| 117 | - else if (deviceType === 2) typeStr = 'ble' | |
| 118 | - else if (deviceType === 3) typeStr = 'dual' | |
| 119 | - if (onDeviceFound) onDeviceFound({ name: deviceName, deviceId, type: typeStr }) | |
| 230 | + let deviceType = invoke(device, 'getType') | |
| 231 | + let deviceId = invoke(device, 'getAddress') | |
| 232 | + let deviceName = invoke(device, 'getName') | |
| 233 | + let newDevice = { | |
| 234 | + name: deviceName != null ? String(deviceName).trim() || 'Unknown Device' : 'Unknown Device', | |
| 235 | + deviceId: String(deviceId || ''), | |
| 236 | + type: normalizeDeviceType(deviceType), | |
| 237 | + } | |
| 238 | + options.discoveryDeviceCallback && options.discoveryDeviceCallback(newDevice) | |
| 120 | 239 | } |
| 121 | 240 | if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED === action) { |
| 122 | - try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} | |
| 123 | - btFindReceiver = null | |
| 124 | - if (onDiscoveryFinished) onDiscoveryFinished() | |
| 241 | + blueToothTool.cancelDiscovery() | |
| 242 | + options.discoveryFinishedCallback && options.discoveryFinishedCallback() | |
| 125 | 243 | } |
| 126 | 244 | }, |
| 127 | 245 | }) |
| 128 | - const filter = new IntentFilter() | |
| 246 | + let filter = new IntentFilter() | |
| 129 | 247 | filter.addAction(BluetoothDevice.ACTION_FOUND) |
| 130 | 248 | filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) |
| 131 | 249 | activity.registerReceiver(btFindReceiver, filter) |
| 132 | 250 | btAdapter.startDiscovery() |
| 251 | + this.state.discoveryDeviceState = true | |
| 133 | 252 | // #endif |
| 134 | 253 | }, |
| 135 | - /** 停止经典蓝牙扫描 */ | |
| 136 | - cancelClassicDiscovery () { | |
| 254 | + startClassicDiscovery (onDeviceFound, onDiscoveryFinished) { | |
| 255 | + this.init({ | |
| 256 | + discoveryDeviceCallback: onDeviceFound || function () {}, | |
| 257 | + discoveryFinishedCallback: onDiscoveryFinished || function () {}, | |
| 258 | + }) | |
| 259 | + this.discoveryNewDevice() | |
| 260 | + }, | |
| 261 | + listenBluetoothStatus () { | |
| 137 | 262 | // #ifdef APP-PLUS |
| 138 | - const btAdapter = getAdapter() | |
| 139 | - const activity = getActivity() | |
| 140 | - if (btAdapter && btAdapter.isDiscovering()) btAdapter.cancelDiscovery() | |
| 141 | - if (btFindReceiver && activity) { | |
| 142 | - try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} | |
| 143 | - btFindReceiver = null | |
| 263 | + if (activity == null) return | |
| 264 | + if (btStatusReceiver != null) { | |
| 265 | + try { activity.unregisterReceiver(btStatusReceiver) } catch (e) { console.error(e) } | |
| 266 | + btStatusReceiver = null | |
| 267 | + } | |
| 268 | + btStatusReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', { | |
| 269 | + onReceive: (context, intent) => { | |
| 270 | + plus.android.importClass(context) | |
| 271 | + plus.android.importClass(intent) | |
| 272 | + let action = intent.getAction() | |
| 273 | + if (action === BluetoothAdapter.ACTION_STATE_CHANGED) { | |
| 274 | + let blueState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0) | |
| 275 | + let stateStr = '' | |
| 276 | + switch (blueState) { | |
| 277 | + case BluetoothAdapter.STATE_TURNING_ON: | |
| 278 | + stateStr = 'STATE_TURNING_ON' | |
| 279 | + break | |
| 280 | + case BluetoothAdapter.STATE_ON: | |
| 281 | + stateStr = 'STATE_ON' | |
| 282 | + this.state.bluetoothEnable = true | |
| 283 | + break | |
| 284 | + case BluetoothAdapter.STATE_TURNING_OFF: | |
| 285 | + stateStr = 'STATE_TURNING_OFF' | |
| 286 | + break | |
| 287 | + case BluetoothAdapter.STATE_OFF: | |
| 288 | + stateStr = 'STATE_OFF' | |
| 289 | + this.state.bluetoothEnable = false | |
| 290 | + break | |
| 291 | + } | |
| 292 | + this.state.bluetoothState = stateStr | |
| 293 | + this.options.listenBTStatusCallback && this.options.listenBTStatusCallback(stateStr) | |
| 294 | + } | |
| 295 | + }, | |
| 296 | + }) | |
| 297 | + let filter = new IntentFilter() | |
| 298 | + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED) | |
| 299 | + activity.registerReceiver(btStatusReceiver, filter) | |
| 300 | + if (this.state.bluetoothEnable) { | |
| 301 | + this.options.listenBTStatusCallback && this.options.listenBTStatusCallback('STATE_ON') | |
| 144 | 302 | } |
| 145 | 303 | // #endif |
| 146 | 304 | }, |
| 147 | - /** 连接经典蓝牙设备(含 D320FAX Virtual BT Printer,支持 RFCOMM 回退) */ | |
| 148 | 305 | connDevice (address, callback) { |
| 149 | 306 | // #ifdef APP-PLUS |
| 307 | + this.cancelDiscovery() | |
| 150 | 308 | if (btSocket != null) this.closeBtSocket() |
| 151 | 309 | this.state.readThreadState = false |
| 152 | - const btAdapter = getAdapter() | |
| 153 | - if (!btAdapter) { | |
| 310 | + this.state.connectionState = 'connecting' | |
| 311 | + this.state.lastAddress = String(address || '') | |
| 312 | + this.state.outputReady = false | |
| 313 | + this.clearErrorState() | |
| 314 | + | |
| 315 | + if (btAdapter == null) { | |
| 154 | 316 | this.shortToast('Bluetooth not available') |
| 155 | - if (callback) callback(false) | |
| 156 | - return false | |
| 157 | - } | |
| 158 | - const inv = getInvoke() | |
| 159 | - const uuid = getMyUuid() | |
| 160 | - if (!inv || !uuid) { | |
| 161 | - this.shortToast('Bluetooth not ready') | |
| 162 | - if (callback) callback(false) | |
| 317 | + this.state.connectionState = 'error' | |
| 318 | + this.setErrorState('Bluetooth not available') | |
| 319 | + callback && callback(false) | |
| 163 | 320 | return false |
| 164 | 321 | } |
| 165 | 322 | try { |
| 166 | - const device = inv(btAdapter, 'getRemoteDevice', address) | |
| 167 | - btSocket = inv(device, 'createRfcommSocketToServiceRecord', uuid) | |
| 168 | - } catch (e) { | |
| 169 | - console.warn('createRfcommSocketToServiceRecord failed, try fallback:', e) | |
| 170 | - try { | |
| 171 | - const device = inv(btAdapter, 'getRemoteDevice', address) | |
| 172 | - const cls = inv(device, 'getClass') | |
| 173 | - const intClass = plus.android.importClass('java.lang.Integer').TYPE | |
| 174 | - const m = inv(cls, 'getMethod', 'createRfcommSocket', intClass) | |
| 175 | - btSocket = inv(m, 'invoke', device, 1) | |
| 176 | - } catch (e2) { | |
| 177 | - console.error('RFCOMM fallback failed:', e2) | |
| 178 | - this.shortToast('Connect failed') | |
| 179 | - if (callback) callback(false) | |
| 323 | + let device = invoke(btAdapter, 'getRemoteDevice', address) | |
| 324 | + const candidates = createSocketCandidates(device) | |
| 325 | + let socket = null | |
| 326 | + let lastError = null | |
| 327 | + for (let i = 0; i < candidates.length; i++) { | |
| 328 | + const candidate = candidates[i] | |
| 329 | + try { | |
| 330 | + socket = candidate.create() | |
| 331 | + if (socket) { | |
| 332 | + this.state.lastSocketStrategy = candidate.name | |
| 333 | + break | |
| 334 | + } | |
| 335 | + } catch (e) { | |
| 336 | + lastError = e | |
| 337 | + console.warn('create socket failed:', candidate.name, e) | |
| 338 | + } | |
| 339 | + } | |
| 340 | + btSocket = socket | |
| 341 | + if (!btSocket) { | |
| 342 | + this.state.connectionState = 'error' | |
| 343 | + this.setErrorState(lastError ? getErrorMessage(lastError) : 'Unable to create Bluetooth socket') | |
| 344 | + callback && callback(false) | |
| 180 | 345 | return false |
| 181 | 346 | } |
| 347 | + } catch (e) { | |
| 348 | + console.error(e) | |
| 349 | + this.state.connectionState = 'error' | |
| 350 | + this.setErrorState(getErrorMessage(e)) | |
| 351 | + callback && callback(false) | |
| 352 | + return false | |
| 182 | 353 | } |
| 354 | + | |
| 183 | 355 | try { |
| 184 | - inv(btSocket, 'connect') | |
| 185 | - btInStream = inv(btSocket, 'getInputStream') | |
| 186 | - btOutStream = inv(btSocket, 'getOutputStream') | |
| 187 | - this.state.readThreadState = true | |
| 188 | - this.shortToast('Connected') | |
| 189 | - if (callback) callback(true) | |
| 356 | + invoke(btSocket, 'connect') | |
| 357 | + const streamReady = this.readData() | |
| 358 | + if (!streamReady) { | |
| 359 | + throw new Error(this.state.lastError || 'Bluetooth output stream not ready') | |
| 360 | + } | |
| 361 | + this.state.connectionState = 'connected' | |
| 362 | + this.shortToast('Classic Bluetooth connected') | |
| 363 | + callback && callback(true) | |
| 190 | 364 | } catch (e) { |
| 191 | - try { btSocket.close() } catch (_) {} | |
| 192 | - btSocket = null | |
| 193 | - this.shortToast('Connect failed') | |
| 194 | - if (callback) callback(false) | |
| 365 | + console.error(e) | |
| 366 | + this.state.connectionState = 'error' | |
| 367 | + this.setErrorState(getErrorMessage(e)) | |
| 368 | + callback && callback(false) | |
| 369 | + try { | |
| 370 | + btSocket.close() | |
| 371 | + btSocket = null | |
| 372 | + } catch (e1) { | |
| 373 | + console.error(e1) | |
| 374 | + } | |
| 375 | + btInStream = null | |
| 376 | + btOutStream = null | |
| 195 | 377 | return false |
| 196 | 378 | } |
| 197 | 379 | return true |
| 198 | 380 | // #endif |
| 199 | - if (callback) callback(false) | |
| 381 | + callback && callback(false) | |
| 200 | 382 | return false |
| 201 | 383 | }, |
| 202 | 384 | disConnDevice () { |
| 203 | 385 | // #ifdef APP-PLUS |
| 204 | - this.closeBtSocket() | |
| 386 | + if (btSocket != null) this.closeBtSocket() | |
| 205 | 387 | this.state.readThreadState = false |
| 206 | - this.shortToast('Disconnected') | |
| 207 | 388 | // #endif |
| 208 | 389 | }, |
| 209 | 390 | closeBtSocket () { |
| 210 | 391 | // #ifdef APP-PLUS |
| 211 | 392 | this.state.readThreadState = false |
| 212 | - if (btSocket) { | |
| 213 | - try { btSocket.close() } catch (_) {} | |
| 214 | - btSocket = null | |
| 215 | - btInStream = null | |
| 216 | - btOutStream = null | |
| 393 | + this.state.outputReady = false | |
| 394 | + this.state.connectionState = 'idle' | |
| 395 | + clearInterval(setIntervalId) | |
| 396 | + setIntervalId = 0 | |
| 397 | + if (!btSocket) return | |
| 398 | + try { | |
| 399 | + btSocket.close() | |
| 400 | + } catch (e) { | |
| 401 | + console.error(e) | |
| 217 | 402 | } |
| 403 | + btSocket = null | |
| 404 | + btInStream = null | |
| 405 | + btOutStream = null | |
| 218 | 406 | // #endif |
| 219 | 407 | }, |
| 220 | - sendByteData (byteData) { | |
| 408 | + cancelDiscovery () { | |
| 409 | + // #ifdef APP-PLUS | |
| 410 | + if (btAdapter != null && btAdapter.isDiscovering()) { | |
| 411 | + btAdapter.cancelDiscovery() | |
| 412 | + } | |
| 413 | + if (btFindReceiver != null && activity != null) { | |
| 414 | + try { activity.unregisterReceiver(btFindReceiver) } catch (_) {} | |
| 415 | + btFindReceiver = null | |
| 416 | + } | |
| 417 | + this.state.discoveryDeviceState = false | |
| 418 | + // #endif | |
| 419 | + }, | |
| 420 | + cancelClassicDiscovery () { | |
| 421 | + this.cancelDiscovery() | |
| 422 | + }, | |
| 423 | + readData () { | |
| 424 | + // #ifdef APP-PLUS | |
| 425 | + if (!btSocket) { | |
| 426 | + this.shortToast('Please connect Bluetooth device first.') | |
| 427 | + this.state.connectionState = 'error' | |
| 428 | + this.setErrorState('Please connect Bluetooth device first.') | |
| 429 | + return false | |
| 430 | + } | |
| 431 | + try { | |
| 432 | + btInStream = invoke(btSocket, 'getInputStream') | |
| 433 | + btOutStream = invoke(btSocket, 'getOutputStream') | |
| 434 | + } catch (e) { | |
| 435 | + console.error(e) | |
| 436 | + this.setErrorState(getErrorMessage(e)) | |
| 437 | + this.closeBtSocket() | |
| 438 | + return false | |
| 439 | + } | |
| 440 | + this.read() | |
| 441 | + this.state.readThreadState = true | |
| 442 | + this.state.outputReady = !!btOutStream | |
| 443 | + return true | |
| 444 | + // #endif | |
| 445 | + return false | |
| 446 | + }, | |
| 447 | + read () { | |
| 448 | + // #ifdef APP-PLUS | |
| 449 | + clearInterval(setIntervalId) | |
| 450 | + setIntervalId = setInterval(() => { | |
| 451 | + if (this.state.readThreadState) { | |
| 452 | + let start = new Date().getTime() | |
| 453 | + let dataArr = [] | |
| 454 | + try { | |
| 455 | + while (btInStream && invoke(btInStream, 'available') !== 0) { | |
| 456 | + let data = invoke(btInStream, 'read') | |
| 457 | + dataArr.push(data) | |
| 458 | + let current = new Date().getTime() | |
| 459 | + if (current - start > 20) break | |
| 460 | + } | |
| 461 | + } catch (e) { | |
| 462 | + this.state.readThreadState = false | |
| 463 | + this.state.connectionState = 'error' | |
| 464 | + this.setErrorState(getErrorMessage(e)) | |
| 465 | + this.options.connExceptionCallback && this.options.connExceptionCallback(e) | |
| 466 | + } | |
| 467 | + if (dataArr.length > 0) { | |
| 468 | + this.options.readDataCallback && this.options.readDataCallback(dataArr) | |
| 469 | + } | |
| 470 | + } | |
| 471 | + }, 40) | |
| 472 | + // #endif | |
| 473 | + }, | |
| 474 | + sendData (dataStr) { | |
| 221 | 475 | // #ifdef APP-PLUS |
| 222 | 476 | if (!btOutStream) { |
| 477 | + this.shortToast('Output stream not ready') | |
| 478 | + return false | |
| 479 | + } | |
| 480 | + let bytes = invoke(dataStr, 'getBytes', 'gb18030') | |
| 481 | + try { | |
| 482 | + this.sendByteData(bytes) | |
| 483 | + } catch (e) { | |
| 484 | + return false | |
| 485 | + } | |
| 486 | + return true | |
| 487 | + // #endif | |
| 488 | + return false | |
| 489 | + }, | |
| 490 | + sendByteData (byteData) { | |
| 491 | + // #ifdef APP-PLUS | |
| 492 | + if (!this.ensureConnection(this.state.lastAddress)) { | |
| 223 | 493 | this.shortToast('Not connected') |
| 224 | 494 | return false |
| 225 | 495 | } |
| 226 | 496 | try { |
| 227 | - const CHUNK_SIZE = 4096 | |
| 497 | + const CHUNK_SIZE = 512 | |
| 498 | + this.state.lastSendMode = 'chunk-write' | |
| 499 | + this.state.lastSendError = '' | |
| 228 | 500 | for (let i = 0; i < byteData.length; i += CHUNK_SIZE) { |
| 229 | - const chunk = byteData.slice(i, i + CHUNK_SIZE) | |
| 230 | - btOutStream.write(chunk) | |
| 501 | + const chunk = normalizeWriteChunk(byteData, i, Math.min(i + CHUNK_SIZE, byteData.length)) | |
| 502 | + try { | |
| 503 | + btOutStream.write(chunk) | |
| 504 | + } catch (writeChunkError) { | |
| 505 | + this.state.lastSendMode = 'byte-write-fallback' | |
| 506 | + for (let j = 0; j < chunk.length; j++) { | |
| 507 | + invoke(btOutStream, 'write', normalizeWriteByte(chunk[j])) | |
| 508 | + } | |
| 509 | + } | |
| 231 | 510 | } |
| 511 | + invoke(btOutStream, 'flush') | |
| 232 | 512 | return true |
| 233 | 513 | } catch (e) { |
| 514 | + const message = getErrorMessage(e) | |
| 515 | + console.error('sendByteData failed:', e) | |
| 516 | + this.setErrorState(message, 'send') | |
| 517 | + this.state.connectionState = 'error' | |
| 234 | 518 | return false |
| 235 | 519 | } |
| 236 | 520 | // #endif |
| 237 | 521 | return false |
| 238 | - } | |
| 522 | + }, | |
| 523 | + isSocketConnected () { | |
| 524 | + // #ifdef APP-PLUS | |
| 525 | + if (!btSocket) return false | |
| 526 | + try { | |
| 527 | + return !!invoke(btSocket, 'isConnected') | |
| 528 | + } catch (_) { | |
| 529 | + return false | |
| 530 | + } | |
| 531 | + // #endif | |
| 532 | + return false | |
| 533 | + }, | |
| 534 | + ensureConnection (address) { | |
| 535 | + // #ifdef APP-PLUS | |
| 536 | + const targetAddress = String(address || this.state.lastAddress || '') | |
| 537 | + if (targetAddress) { | |
| 538 | + this.state.lastAddress = targetAddress | |
| 539 | + } | |
| 540 | + if (btOutStream && this.isSocketConnected()) { | |
| 541 | + this.state.outputReady = true | |
| 542 | + return true | |
| 543 | + } | |
| 544 | + if (!targetAddress) { | |
| 545 | + this.setErrorState('Bluetooth address missing') | |
| 546 | + return false | |
| 547 | + } | |
| 548 | + let connected = false | |
| 549 | + this.connDevice(targetAddress, (ok) => { | |
| 550 | + connected = !!ok | |
| 551 | + }) | |
| 552 | + return connected && !!btOutStream | |
| 553 | + // #endif | |
| 554 | + return false | |
| 555 | + }, | |
| 556 | + getLastError () { | |
| 557 | + return this.state.lastSendError || this.state.lastError || '' | |
| 558 | + }, | |
| 559 | + getDebugState () { | |
| 560 | + return { | |
| 561 | + ...this.state, | |
| 562 | + socketConnected: this.isSocketConnected(), | |
| 563 | + outputReady: !!btOutStream, | |
| 564 | + inputReady: !!btInStream, | |
| 565 | + } | |
| 566 | + }, | |
| 239 | 567 | } |
| 240 | 568 | |
| 241 | 569 | export default blueToothTool | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/d320fax.ts
0 → 100644
| 1 | +import { buildTscLabelData, buildTscTestPrintData } from '../protocols/tscProtocol' | |
| 2 | +import type { PrinterCandidate, PrinterDriver } from '../types/printer' | |
| 3 | + | |
| 4 | +const KEYWORDS = [ | |
| 5 | + 'd320fax', | |
| 6 | + 'd320fx', | |
| 7 | + 'virtual bt printer', | |
| 8 | + 'gp-d320fax', | |
| 9 | +] | |
| 10 | + | |
| 11 | +function score (device: PrinterCandidate): number { | |
| 12 | + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase() | |
| 13 | + let total = 0 | |
| 14 | + KEYWORDS.forEach((keyword) => { | |
| 15 | + if (text.includes(keyword)) total += 40 | |
| 16 | + }) | |
| 17 | + return total | |
| 18 | +} | |
| 19 | + | |
| 20 | +export const d320faxDriver: PrinterDriver = { | |
| 21 | + key: 'd320fax', | |
| 22 | + brand: 'Gprinter', | |
| 23 | + model: 'D320FAX/D320FX', | |
| 24 | + displayName: 'Gprinter D320FAX', | |
| 25 | + protocol: 'tsc', | |
| 26 | + preferredConnection: 'classic', | |
| 27 | + preferredBleMtu: 20, | |
| 28 | + keywords: KEYWORDS, | |
| 29 | + matches (device) { | |
| 30 | + return score(device) | |
| 31 | + }, | |
| 32 | + resolveConnectionType () { | |
| 33 | + return 'classic' | |
| 34 | + }, | |
| 35 | + buildTestPrintData () { | |
| 36 | + return buildTscTestPrintData() | |
| 37 | + }, | |
| 38 | + buildLabelData (payload) { | |
| 39 | + return buildTscLabelData(payload) | |
| 40 | + }, | |
| 41 | +} | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/genericTsc.ts
0 → 100644
| 1 | +import { buildTscLabelData, buildTscTestPrintData } from '../protocols/tscProtocol' | |
| 2 | +import type { PrinterCandidate, PrinterDriver } from '../types/printer' | |
| 3 | + | |
| 4 | +const GENERIC_TSC_KEYWORDS = [ | |
| 5 | + 'printer', 'print', 'label', 'tsc', 'zebra', 'brother', 'epson', 'godex', | |
| 6 | + 'citizen', 'ql-', 'zd', 'zt', 'ttp', 'tdp', 'bt printer', 'virtual bt printer', | |
| 7 | +] | |
| 8 | + | |
| 9 | +function scoreByKeywords (device: PrinterCandidate, keywords: string[]): number { | |
| 10 | + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase() | |
| 11 | + let score = 0 | |
| 12 | + keywords.forEach((keyword) => { | |
| 13 | + if (text.includes(keyword)) score += 10 | |
| 14 | + }) | |
| 15 | + return score | |
| 16 | +} | |
| 17 | + | |
| 18 | +export const genericTscDriver: PrinterDriver = { | |
| 19 | + key: 'generic-tsc', | |
| 20 | + brand: 'Generic', | |
| 21 | + model: 'TSC', | |
| 22 | + displayName: 'Generic TSC Printer', | |
| 23 | + protocol: 'tsc', | |
| 24 | + preferredBleMtu: 20, | |
| 25 | + keywords: GENERIC_TSC_KEYWORDS, | |
| 26 | + matches (device) { | |
| 27 | + return scoreByKeywords(device, GENERIC_TSC_KEYWORDS) | |
| 28 | + }, | |
| 29 | + resolveConnectionType (device) { | |
| 30 | + return device.type === 'ble' ? 'ble' : 'classic' | |
| 31 | + }, | |
| 32 | + buildTestPrintData () { | |
| 33 | + return buildTscTestPrintData() | |
| 34 | + }, | |
| 35 | + buildLabelData (payload) { | |
| 36 | + return buildTscLabelData(payload) | |
| 37 | + }, | |
| 38 | +} | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/gpR3.ts
0 → 100644
| 1 | +import { buildEscPosLabelData, buildEscPosTestPrintData } from '../protocols/escPosBuilder' | |
| 2 | +import type { PrinterCandidate, PrinterDriver } from '../types/printer' | |
| 3 | + | |
| 4 | +const KEYWORDS = [ | |
| 5 | + 'gp-r3', | |
| 6 | + 'gp r3', | |
| 7 | + 'gpr3', | |
| 8 | +] | |
| 9 | + | |
| 10 | +function score (device: PrinterCandidate): number { | |
| 11 | + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase() | |
| 12 | + let total = 0 | |
| 13 | + KEYWORDS.forEach((keyword) => { | |
| 14 | + if (text.includes(keyword)) total += 60 | |
| 15 | + }) | |
| 16 | + if (text.includes('gprinter')) total += 10 | |
| 17 | + return total | |
| 18 | +} | |
| 19 | + | |
| 20 | +export const gpR3Driver: PrinterDriver = { | |
| 21 | + key: 'gp-r3', | |
| 22 | + brand: 'Gprinter', | |
| 23 | + model: 'GP-R3', | |
| 24 | + displayName: 'Gprinter GP-R3', | |
| 25 | + protocol: 'esc', | |
| 26 | + preferredConnection: 'classic', | |
| 27 | + preferredBleMtu: 20, | |
| 28 | + keywords: KEYWORDS, | |
| 29 | + matches (device) { | |
| 30 | + return score(device) | |
| 31 | + }, | |
| 32 | + resolveConnectionType (device) { | |
| 33 | + if (device.type === 'ble') return 'ble' | |
| 34 | + return 'classic' | |
| 35 | + }, | |
| 36 | + buildTestPrintData () { | |
| 37 | + return buildEscPosTestPrintData() | |
| 38 | + }, | |
| 39 | + buildLabelData (payload) { | |
| 40 | + return buildEscPosLabelData(payload) | |
| 41 | + }, | |
| 42 | +} | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/manager/driverRegistry.ts
0 → 100644
| 1 | +import { d320faxDriver } from '../drivers/d320fax' | |
| 2 | +import { genericTscDriver } from '../drivers/genericTsc' | |
| 3 | +import { gpR3Driver } from '../drivers/gpR3' | |
| 4 | +import type { PrinterCandidate, PrinterDriver, ResolvedPrinterCandidate } from '../types/printer' | |
| 5 | + | |
| 6 | +const printerDrivers: PrinterDriver[] = [ | |
| 7 | + gpR3Driver, | |
| 8 | + d320faxDriver, | |
| 9 | + genericTscDriver, | |
| 10 | +] | |
| 11 | + | |
| 12 | +export function getPrinterDrivers (): PrinterDriver[] { | |
| 13 | + return printerDrivers | |
| 14 | +} | |
| 15 | + | |
| 16 | +export function getPrinterDriverByKey (key?: string): PrinterDriver { | |
| 17 | + return printerDrivers.find(driver => driver.key === key) || genericTscDriver | |
| 18 | +} | |
| 19 | + | |
| 20 | +export function resolvePrinterDriver (device: PrinterCandidate): PrinterDriver { | |
| 21 | + let bestDriver: PrinterDriver = genericTscDriver | |
| 22 | + let bestScore = -1 | |
| 23 | + printerDrivers.forEach((driver) => { | |
| 24 | + const score = driver.matches(device) | |
| 25 | + if (score > bestScore) { | |
| 26 | + bestScore = score | |
| 27 | + bestDriver = driver | |
| 28 | + } | |
| 29 | + }) | |
| 30 | + return bestDriver | |
| 31 | +} | |
| 32 | + | |
| 33 | +export function describePrinterCandidate (device: PrinterCandidate): ResolvedPrinterCandidate { | |
| 34 | + const driver = resolvePrinterDriver(device) | |
| 35 | + return { | |
| 36 | + ...device, | |
| 37 | + driverKey: driver.key, | |
| 38 | + driverName: driver.displayName, | |
| 39 | + protocol: driver.protocol, | |
| 40 | + resolvedType: driver.resolveConnectionType(device), | |
| 41 | + } | |
| 42 | +} | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts
0 → 100644
| 1 | +import { | |
| 2 | + clearPrinter, | |
| 3 | + getBluetoothConnection, | |
| 4 | + getCurrentPrinterDriverKey, | |
| 5 | + getPrinterType, | |
| 6 | + sendToPrinter, | |
| 7 | + setBluetoothConnection, | |
| 8 | + setBuiltinPrinter, | |
| 9 | +} from '../printerConnection' | |
| 10 | +import classicBluetooth from '../bluetoothTool.js' | |
| 11 | +import { describePrinterCandidate, getPrinterDriverByKey, resolvePrinterDriver } from './driverRegistry' | |
| 12 | +import type { | |
| 13 | + CurrentPrinterSummary, | |
| 14 | + LabelPrintPayload, | |
| 15 | + PrinterCandidate, | |
| 16 | + PrinterDriver, | |
| 17 | +} from '../types/printer' | |
| 18 | + | |
| 19 | +function connectClassicBluetooth (device: PrinterCandidate, driver: PrinterDriver): Promise<void> { | |
| 20 | + return new Promise((resolve, reject) => { | |
| 21 | + // #ifdef APP-PLUS | |
| 22 | + const classic = classicBluetooth | |
| 23 | + if (!classic || !classic.connDevice) { | |
| 24 | + reject(new Error('Classic Bluetooth not available. Ensure app is running on the device.')) | |
| 25 | + return | |
| 26 | + } | |
| 27 | + classic.connDevice(device.deviceId, (ok: boolean) => { | |
| 28 | + if (!ok) { | |
| 29 | + reject(new Error('Classic Bluetooth connection failed.')) | |
| 30 | + return | |
| 31 | + } | |
| 32 | + setBluetoothConnection({ | |
| 33 | + deviceId: device.deviceId, | |
| 34 | + deviceName: device.name || 'Bluetooth Printer', | |
| 35 | + deviceType: 'classic', | |
| 36 | + driverKey: driver.key, | |
| 37 | + mtu: driver.preferredBleMtu || 20, | |
| 38 | + }) | |
| 39 | + resolve() | |
| 40 | + }) | |
| 41 | + // #endif | |
| 42 | + // #ifndef APP-PLUS | |
| 43 | + reject(new Error('Classic Bluetooth requires the app.')) | |
| 44 | + // #endif | |
| 45 | + }) | |
| 46 | +} | |
| 47 | + | |
| 48 | +function findBleWriteCharacteristic (deviceId: string): Promise<{ serviceId: string; characteristicId: string } | null> { | |
| 49 | + return new Promise((resolve) => { | |
| 50 | + uni.getBLEDeviceServices({ | |
| 51 | + deviceId, | |
| 52 | + success: (serviceRes) => { | |
| 53 | + const services = serviceRes.services || [] | |
| 54 | + const next = (index: number) => { | |
| 55 | + if (index >= services.length) { | |
| 56 | + resolve(null) | |
| 57 | + return | |
| 58 | + } | |
| 59 | + const serviceId = services[index].uuid | |
| 60 | + uni.getBLEDeviceCharacteristics({ | |
| 61 | + deviceId, | |
| 62 | + serviceId, | |
| 63 | + success: (charRes) => { | |
| 64 | + const target = (charRes.characteristics || []).find((item: any) => item.properties && item.properties.write) | |
| 65 | + if (target) { | |
| 66 | + resolve({ | |
| 67 | + serviceId, | |
| 68 | + characteristicId: target.uuid, | |
| 69 | + }) | |
| 70 | + return | |
| 71 | + } | |
| 72 | + next(index + 1) | |
| 73 | + }, | |
| 74 | + fail: () => next(index + 1), | |
| 75 | + }) | |
| 76 | + } | |
| 77 | + next(0) | |
| 78 | + }, | |
| 79 | + fail: () => resolve(null), | |
| 80 | + }) | |
| 81 | + }) | |
| 82 | +} | |
| 83 | + | |
| 84 | +function connectBlePrinter (device: PrinterCandidate, driver: PrinterDriver): Promise<void> { | |
| 85 | + const finalizeExistingBleConnection = async () => { | |
| 86 | + const write = await findBleWriteCharacteristic(device.deviceId) | |
| 87 | + if (!write) { | |
| 88 | + throw new Error('No writable characteristic found. This device may not support printing.') | |
| 89 | + } | |
| 90 | + setBluetoothConnection({ | |
| 91 | + deviceId: device.deviceId, | |
| 92 | + deviceName: device.name || 'Bluetooth Printer', | |
| 93 | + serviceId: write.serviceId, | |
| 94 | + characteristicId: write.characteristicId, | |
| 95 | + deviceType: 'ble', | |
| 96 | + mtu: driver.preferredBleMtu || 20, | |
| 97 | + driverKey: driver.key, | |
| 98 | + }) | |
| 99 | + } | |
| 100 | + | |
| 101 | + return new Promise((resolve, reject) => { | |
| 102 | + uni.createBLEConnection({ | |
| 103 | + deviceId: device.deviceId, | |
| 104 | + timeout: 10000, | |
| 105 | + success: async () => { | |
| 106 | + try { | |
| 107 | + await finalizeExistingBleConnection() | |
| 108 | + resolve() | |
| 109 | + } catch (e: any) { | |
| 110 | + reject(e) | |
| 111 | + } | |
| 112 | + }, | |
| 113 | + fail: (err: any) => { | |
| 114 | + if (err?.errCode === -1) { | |
| 115 | + finalizeExistingBleConnection().then(() => resolve()).catch(reject) | |
| 116 | + } else { | |
| 117 | + reject(new Error(err?.errMsg || 'BLE connection failed.')) | |
| 118 | + } | |
| 119 | + }, | |
| 120 | + }) | |
| 121 | + }) | |
| 122 | +} | |
| 123 | + | |
| 124 | +export async function connectBluetoothPrinter (device: PrinterCandidate): Promise<PrinterDriver> { | |
| 125 | + const driver = resolvePrinterDriver(device) | |
| 126 | + const resolvedType = driver.resolveConnectionType(device) | |
| 127 | + if (resolvedType === 'classic') { | |
| 128 | + await connectClassicBluetooth(device, driver) | |
| 129 | + } else { | |
| 130 | + await connectBlePrinter(device, driver) | |
| 131 | + } | |
| 132 | + return driver | |
| 133 | +} | |
| 134 | + | |
| 135 | +export function useBuiltinPrinter (driverKey = 'generic-tsc') { | |
| 136 | + setBuiltinPrinter(driverKey) | |
| 137 | +} | |
| 138 | + | |
| 139 | +export function getCurrentPrinterDriver (): PrinterDriver { | |
| 140 | + const type = getPrinterType() | |
| 141 | + const storedKey = getCurrentPrinterDriverKey() | |
| 142 | + if (storedKey) return getPrinterDriverByKey(storedKey) | |
| 143 | + if (type === 'bluetooth') { | |
| 144 | + const connection = getBluetoothConnection() | |
| 145 | + if (connection) { | |
| 146 | + return resolvePrinterDriver({ | |
| 147 | + deviceId: connection.deviceId, | |
| 148 | + name: connection.deviceName, | |
| 149 | + type: connection.deviceType, | |
| 150 | + }) | |
| 151 | + } | |
| 152 | + } | |
| 153 | + return getPrinterDriverByKey('generic-tsc') | |
| 154 | +} | |
| 155 | + | |
| 156 | +export function getCurrentPrinterSummary (): CurrentPrinterSummary { | |
| 157 | + const type = getPrinterType() | |
| 158 | + const driver = getCurrentPrinterDriver() | |
| 159 | + if (type === 'builtin') { | |
| 160 | + return { | |
| 161 | + type, | |
| 162 | + displayName: 'Built-in Printer', | |
| 163 | + deviceId: 'builtin', | |
| 164 | + driverKey: driver.key, | |
| 165 | + driverName: driver.displayName, | |
| 166 | + protocol: driver.protocol, | |
| 167 | + deviceType: '', | |
| 168 | + } | |
| 169 | + } | |
| 170 | + if (type === 'bluetooth') { | |
| 171 | + const connection = getBluetoothConnection() | |
| 172 | + if (connection) { | |
| 173 | + return { | |
| 174 | + type, | |
| 175 | + displayName: connection.deviceName || driver.displayName, | |
| 176 | + deviceId: connection.deviceId, | |
| 177 | + driverKey: driver.key, | |
| 178 | + driverName: driver.displayName, | |
| 179 | + protocol: driver.protocol, | |
| 180 | + deviceType: connection.deviceType, | |
| 181 | + } | |
| 182 | + } | |
| 183 | + } | |
| 184 | + return { | |
| 185 | + type: '', | |
| 186 | + displayName: '', | |
| 187 | + deviceId: '', | |
| 188 | + driverKey: driver.key, | |
| 189 | + driverName: driver.displayName, | |
| 190 | + protocol: driver.protocol, | |
| 191 | + deviceType: '', | |
| 192 | + } | |
| 193 | +} | |
| 194 | + | |
| 195 | +export async function testPrintCurrentPrinter (onProgress?: (percent: number) => void): Promise<PrinterDriver> { | |
| 196 | + const driver = getCurrentPrinterDriver() | |
| 197 | + await sendToPrinter(driver.buildTestPrintData(), onProgress) | |
| 198 | + return driver | |
| 199 | +} | |
| 200 | + | |
| 201 | +export async function printLabelForCurrentPrinter ( | |
| 202 | + payload: LabelPrintPayload, | |
| 203 | + onProgress?: (percent: number) => void | |
| 204 | +): Promise<PrinterDriver> { | |
| 205 | + const driver = getCurrentPrinterDriver() | |
| 206 | + await sendToPrinter(driver.buildLabelData(payload), onProgress) | |
| 207 | + return driver | |
| 208 | +} | |
| 209 | + | |
| 210 | +export function describeDiscoveredPrinter (device: PrinterCandidate) { | |
| 211 | + return describePrinterCandidate(device) | |
| 212 | +} | |
| 213 | + | |
| 214 | +export function disconnectCurrentPrinter (): Promise<void> { | |
| 215 | + return new Promise((resolve) => { | |
| 216 | + const type = getPrinterType() | |
| 217 | + const connection = getBluetoothConnection() | |
| 218 | + | |
| 219 | + if (type === 'bluetooth' && connection?.deviceType === 'classic') { | |
| 220 | + // #ifdef APP-PLUS | |
| 221 | + try { | |
| 222 | + const classic = classicBluetooth | |
| 223 | + if (classic && classic.disConnDevice) classic.disConnDevice() | |
| 224 | + } catch (e) { | |
| 225 | + console.error('Disconnect classic bluetooth failed', e) | |
| 226 | + } | |
| 227 | + // #endif | |
| 228 | + clearPrinter() | |
| 229 | + resolve() | |
| 230 | + return | |
| 231 | + } | |
| 232 | + | |
| 233 | + clearPrinter() | |
| 234 | + if (type === 'bluetooth' && connection?.deviceId) { | |
| 235 | + uni.closeBLEConnection({ | |
| 236 | + deviceId: connection.deviceId, | |
| 237 | + complete: () => resolve(), | |
| 238 | + }) | |
| 239 | + return | |
| 240 | + } | |
| 241 | + resolve() | |
| 242 | + }) | |
| 243 | +} | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts
| 1 | 1 | /** |
| 2 | 2 | * 打印机连接与下发:蓝牙(BLE) / 一体机(TCP localhost) |
| 3 | 3 | */ |
| 4 | +import type { ActiveBtDeviceType, PrinterType } from './types/printer' | |
| 5 | +import classicBluetooth from './bluetoothTool.js' | |
| 4 | 6 | |
| 5 | 7 | const STORAGE_PRINTER_TYPE = 'printerType' |
| 6 | 8 | const STORAGE_BT_DEVICE_ID = 'btDeviceId' |
| ... | ... | @@ -10,11 +12,10 @@ const STORAGE_BT_CHARACTERISTIC_ID = 'btCharacteristicId' |
| 10 | 12 | const STORAGE_BT_DEVICE_TYPE = 'btDeviceType' // 'ble' | 'classic' |
| 11 | 13 | const STORAGE_BLE_MTU = 'bleMTU' |
| 12 | 14 | const STORAGE_BUILTIN_PORT = 'builtinPort' |
| 15 | +const STORAGE_PRINTER_DRIVER_KEY = 'printerDriverKey' | |
| 13 | 16 | |
| 14 | 17 | const BUILTIN_PROBE_PORTS = [9100, 4000, 9000, 6000] |
| 15 | - | |
| 16 | -export type PrinterType = 'bluetooth' | 'builtin' | |
| 17 | -export type BtDeviceType = 'ble' | 'classic' | |
| 18 | +export type BtDeviceType = ActiveBtDeviceType | |
| 18 | 19 | |
| 19 | 20 | export const PrinterStorageKeys = { |
| 20 | 21 | type: STORAGE_PRINTER_TYPE, |
| ... | ... | @@ -24,6 +25,7 @@ export const PrinterStorageKeys = { |
| 24 | 25 | btCharacteristicId: STORAGE_BT_CHARACTERISTIC_ID, |
| 25 | 26 | btDeviceType: STORAGE_BT_DEVICE_TYPE, |
| 26 | 27 | bleMTU: STORAGE_BLE_MTU, |
| 28 | + driverKey: STORAGE_PRINTER_DRIVER_KEY, | |
| 27 | 29 | } as const |
| 28 | 30 | |
| 29 | 31 | export function setPrinterType (type: PrinterType) { |
| ... | ... | @@ -37,6 +39,7 @@ export function setBluetoothConnection (info: { |
| 37 | 39 | characteristicId?: string |
| 38 | 40 | deviceType?: BtDeviceType |
| 39 | 41 | mtu?: number |
| 42 | + driverKey?: string | |
| 40 | 43 | }) { |
| 41 | 44 | uni.setStorageSync(STORAGE_PRINTER_TYPE, 'bluetooth') |
| 42 | 45 | uni.setStorageSync(STORAGE_BT_DEVICE_ID, info.deviceId) |
| ... | ... | @@ -45,10 +48,12 @@ export function setBluetoothConnection (info: { |
| 45 | 48 | uni.setStorageSync(STORAGE_BT_CHARACTERISTIC_ID, info.characteristicId || '') |
| 46 | 49 | uni.setStorageSync(STORAGE_BT_DEVICE_TYPE, info.deviceType || 'ble') |
| 47 | 50 | uni.setStorageSync(STORAGE_BLE_MTU, info.mtu != null ? info.mtu : BLE_MTU_DEFAULT) |
| 51 | + uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, info.driverKey || '') | |
| 48 | 52 | } |
| 49 | 53 | |
| 50 | -export function setBuiltinPrinter () { | |
| 54 | +export function setBuiltinPrinter (driverKey = 'generic-tsc') { | |
| 51 | 55 | uni.setStorageSync(STORAGE_PRINTER_TYPE, 'builtin') |
| 56 | + uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, driverKey) | |
| 52 | 57 | } |
| 53 | 58 | |
| 54 | 59 | export function clearPrinter () { |
| ... | ... | @@ -60,6 +65,7 @@ export function clearPrinter () { |
| 60 | 65 | uni.removeStorageSync(STORAGE_BT_DEVICE_TYPE) |
| 61 | 66 | uni.removeStorageSync(STORAGE_BLE_MTU) |
| 62 | 67 | uni.removeStorageSync(STORAGE_BUILTIN_PORT) |
| 68 | + uni.removeStorageSync(STORAGE_PRINTER_DRIVER_KEY) | |
| 63 | 69 | } |
| 64 | 70 | |
| 65 | 71 | const BLE_MTU_DEFAULT = 20 |
| ... | ... | @@ -68,6 +74,10 @@ export function getPrinterType (): PrinterType | '' { |
| 68 | 74 | return (uni.getStorageSync(STORAGE_PRINTER_TYPE) as PrinterType) || '' |
| 69 | 75 | } |
| 70 | 76 | |
| 77 | +export function getCurrentPrinterDriverKey (): string { | |
| 78 | + return String(uni.getStorageSync(STORAGE_PRINTER_DRIVER_KEY) || '') | |
| 79 | +} | |
| 80 | + | |
| 71 | 81 | export function getBluetoothConnection (): { |
| 72 | 82 | deviceId: string |
| 73 | 83 | deviceName: string |
| ... | ... | @@ -186,20 +196,31 @@ function sendViaClassic ( |
| 186 | 196 | } |
| 187 | 197 | return new Promise((resolve, reject) => { |
| 188 | 198 | try { |
| 189 | - const classicBluetooth = (require('./bluetoothTool.js') as any).default | |
| 190 | 199 | if (!classicBluetooth) { |
| 191 | 200 | reject(new Error('Classic Bluetooth not available')) |
| 192 | 201 | return |
| 193 | 202 | } |
| 194 | - const sendData = data.map((byte) => { | |
| 195 | - const b = byte & 0xff | |
| 196 | - if (b >= 128) return b % 128 - 128 | |
| 197 | - return b | |
| 198 | - }) | |
| 203 | + const ready = typeof classicBluetooth.ensureConnection === 'function' | |
| 204 | + ? classicBluetooth.ensureConnection(conn.deviceId) | |
| 205 | + : true | |
| 206 | + if (!ready) { | |
| 207 | + const errorMessage = typeof classicBluetooth.getLastError === 'function' | |
| 208 | + ? classicBluetooth.getLastError() | |
| 209 | + : '' | |
| 210 | + reject(new Error(errorMessage || 'Classic Bluetooth connection is not ready')) | |
| 211 | + return | |
| 212 | + } | |
| 213 | + | |
| 214 | + const sendData = data.map((byte) => byte & 0xff) | |
| 199 | 215 | const ok = classicBluetooth.sendByteData(sendData) |
| 200 | 216 | if (onProgress) onProgress(100) |
| 201 | 217 | if (ok) resolve() |
| 202 | - else reject(new Error('Classic Bluetooth send failed')) | |
| 218 | + else { | |
| 219 | + const errorMessage = typeof classicBluetooth.getLastError === 'function' | |
| 220 | + ? classicBluetooth.getLastError() | |
| 221 | + : '' | |
| 222 | + reject(new Error(errorMessage || 'Classic Bluetooth send failed')) | |
| 223 | + } | |
| 203 | 224 | } catch (e: any) { |
| 204 | 225 | reject(e) |
| 205 | 226 | } | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/protocols/escPosBuilder.ts
0 → 100644
| 1 | +import type { LabelPrintPayload } from '../types/printer' | |
| 2 | + | |
| 3 | +function stringToBytes (str: string): number[] { | |
| 4 | + const out: number[] = [] | |
| 5 | + for (let i = 0; i < str.length; i++) { | |
| 6 | + let c = str.charCodeAt(i) | |
| 7 | + if (c < 0x80) { | |
| 8 | + out.push(c) | |
| 9 | + } else if (c < 0x800) { | |
| 10 | + out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)) | |
| 11 | + } else if (c < 0xd800 || c >= 0xe000) { | |
| 12 | + out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)) | |
| 13 | + } else { | |
| 14 | + i++ | |
| 15 | + const c2 = str.charCodeAt(i) | |
| 16 | + const u = ((c & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000 | |
| 17 | + out.push( | |
| 18 | + 0xf0 | (u >> 18), | |
| 19 | + 0x80 | ((u >> 12) & 0x3f), | |
| 20 | + 0x80 | ((u >> 6) & 0x3f), | |
| 21 | + 0x80 | (u & 0x3f) | |
| 22 | + ) | |
| 23 | + } | |
| 24 | + } | |
| 25 | + return out | |
| 26 | +} | |
| 27 | + | |
| 28 | +function appendText (out: number[], text: string) { | |
| 29 | + const bytes = stringToBytes(text) | |
| 30 | + for (let i = 0; i < bytes.length; i++) out.push(bytes[i]) | |
| 31 | +} | |
| 32 | + | |
| 33 | +function appendLine (out: number[], text = '') { | |
| 34 | + appendText(out, text) | |
| 35 | + out.push(0x0a) | |
| 36 | +} | |
| 37 | + | |
| 38 | +function appendAlign (out: number[], align: 0 | 1 | 2) { | |
| 39 | + out.push(0x1b, 0x61, align) | |
| 40 | +} | |
| 41 | + | |
| 42 | +function appendBold (out: number[], bold: boolean) { | |
| 43 | + out.push(0x1b, 0x45, bold ? 1 : 0) | |
| 44 | +} | |
| 45 | + | |
| 46 | +function appendSize (out: number[], width = 0, height = 0) { | |
| 47 | + const value = ((width & 0x07) << 4) | (height & 0x07) | |
| 48 | + out.push(0x1d, 0x21, value) | |
| 49 | +} | |
| 50 | + | |
| 51 | +function createEscDocument (builder: (out: number[]) => void): number[] { | |
| 52 | + const out: number[] = [] | |
| 53 | + out.push(0x1b, 0x40) | |
| 54 | + builder(out) | |
| 55 | + out.push(0x1b, 0x64, 0x04) | |
| 56 | + return out | |
| 57 | +} | |
| 58 | + | |
| 59 | +export function buildEscPosTestPrintData (): number[] { | |
| 60 | + return createEscDocument((out) => { | |
| 61 | + appendAlign(out, 1) | |
| 62 | + appendBold(out, true) | |
| 63 | + appendSize(out, 1, 1) | |
| 64 | + appendLine(out, 'TEST PRINT') | |
| 65 | + appendBold(out, false) | |
| 66 | + appendSize(out, 0, 0) | |
| 67 | + appendLine(out, 'GP-R3 / ESC-POS') | |
| 68 | + appendLine(out, 'Food Label System') | |
| 69 | + appendLine(out, '-----------------------------') | |
| 70 | + appendLine(out, 'Connection OK') | |
| 71 | + appendLine(out, 'Protocol OK') | |
| 72 | + appendLine(out, '-----------------------------') | |
| 73 | + }) | |
| 74 | +} | |
| 75 | + | |
| 76 | +export function buildEscPosLabelData (payload: LabelPrintPayload): number[] { | |
| 77 | + const { | |
| 78 | + productName, | |
| 79 | + labelId, | |
| 80 | + printQty = 1, | |
| 81 | + category = '', | |
| 82 | + extraLine = '', | |
| 83 | + } = payload | |
| 84 | + | |
| 85 | + return createEscDocument((out) => { | |
| 86 | + appendAlign(out, 1) | |
| 87 | + appendBold(out, true) | |
| 88 | + appendSize(out, 1, 1) | |
| 89 | + appendLine(out, 'FOOD LABEL') | |
| 90 | + appendBold(out, false) | |
| 91 | + appendSize(out, 0, 0) | |
| 92 | + appendLine(out, '-----------------------------') | |
| 93 | + appendLine(out, 'Product: ' + productName) | |
| 94 | + if (category) appendLine(out, 'Category: ' + category) | |
| 95 | + appendLine(out, 'Label ID: ' + labelId) | |
| 96 | + if (extraLine) appendLine(out, extraLine) | |
| 97 | + appendLine(out, 'Qty: ' + String(printQty)) | |
| 98 | + appendLine(out, '-----------------------------') | |
| 99 | + }) | |
| 100 | +} | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/protocols/tscProtocol.ts
0 → 100644
| 1 | +import type { LabelPrintPayload } from '../types/printer' | |
| 2 | +import { buildTestTscLabel, buildTscLabel } from '../tscLabelBuilder' | |
| 3 | + | |
| 4 | +export function buildTscTestPrintData (): number[] { | |
| 5 | + return buildTestTscLabel() | |
| 6 | +} | |
| 7 | + | |
| 8 | +export function buildTscLabelData (payload: LabelPrintPayload): number[] { | |
| 9 | + return buildTscLabel(payload) | |
| 10 | +} | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/types/printer.ts
0 → 100644
| 1 | +export type PrinterType = 'bluetooth' | 'builtin' | |
| 2 | +export type BtDeviceType = 'ble' | 'classic' | 'dual' | 'unknown' | |
| 3 | +export type ActiveBtDeviceType = 'ble' | 'classic' | |
| 4 | +export type PrinterProtocol = 'tsc' | 'esc' | |
| 5 | + | |
| 6 | +export interface PrinterCandidate { | |
| 7 | + deviceId: string | |
| 8 | + name: string | |
| 9 | + RSSI?: number | |
| 10 | + type?: BtDeviceType | |
| 11 | +} | |
| 12 | + | |
| 13 | +export interface LabelPrintPayload { | |
| 14 | + productName: string | |
| 15 | + labelId: string | |
| 16 | + printQty?: number | |
| 17 | + widthMm?: number | |
| 18 | + heightMm?: number | |
| 19 | + category?: string | |
| 20 | + extraLine?: string | |
| 21 | +} | |
| 22 | + | |
| 23 | +export interface PrinterDriver { | |
| 24 | + key: string | |
| 25 | + brand: string | |
| 26 | + model: string | |
| 27 | + displayName: string | |
| 28 | + protocol: PrinterProtocol | |
| 29 | + preferredConnection?: ActiveBtDeviceType | |
| 30 | + preferredBleMtu?: number | |
| 31 | + keywords: string[] | |
| 32 | + matches: (device: PrinterCandidate) => number | |
| 33 | + resolveConnectionType: (device: PrinterCandidate) => ActiveBtDeviceType | |
| 34 | + buildTestPrintData: () => number[] | |
| 35 | + buildLabelData: (payload: LabelPrintPayload) => number[] | |
| 36 | +} | |
| 37 | + | |
| 38 | +export interface ResolvedPrinterCandidate extends PrinterCandidate { | |
| 39 | + driverKey: string | |
| 40 | + driverName: string | |
| 41 | + protocol: PrinterProtocol | |
| 42 | + resolvedType: ActiveBtDeviceType | |
| 43 | +} | |
| 44 | + | |
| 45 | +export interface CurrentPrinterSummary { | |
| 46 | + type: PrinterType | '' | |
| 47 | + displayName: string | |
| 48 | + deviceId: string | |
| 49 | + driverKey: string | |
| 50 | + driverName: string | |
| 51 | + protocol: PrinterProtocol | |
| 52 | + deviceType?: ActiveBtDeviceType | '' | |
| 53 | +} | ... | ... |