Commit 961eecae3d8a442b5739a022bebd769929b22978

Authored by “wangming”
1 parent 0afa2a00

对打印机进行开发

美国版/Food Labeling Management App UniApp/src/manifest.json
@@ -2,8 +2,8 @@ @@ -2,8 +2,8 @@
2 "name" : "food.labeling", 2 "name" : "food.labeling",
3 "appid" : "__UNI__1BFD76D", 3 "appid" : "__UNI__1BFD76D",
4 "description" : "", 4 "description" : "",
5 - "versionName" : "1.0.1",  
6 - "versionCode" : 101, 5 + "versionName" : "1.0.3",
  6 + "versionCode" : 103,
7 "transformPx" : false, 7 "transformPx" : false,
8 /* 5+App特有相关 */ 8 /* 5+App特有相关 */
9 "app-plus" : { 9 "app-plus" : {
@@ -18,8 +18,9 @@ @@ -18,8 +18,9 @@
18 }, 18 },
19 /* 模块配置 */ 19 /* 模块配置 */
20 "modules" : { 20 "modules" : {
21 - "Bluetooth" : {},  
22 - "Camera" : {} 21 + "Camera" : {},
  22 + "Barcode" : {},
  23 + "Bluetooth" : {}
23 }, 24 },
24 /* 应用发布信息 */ 25 /* 应用发布信息 */
25 "distribute" : { 26 "distribute" : {
@@ -43,6 +44,10 @@ @@ -43,6 +44,10 @@
43 "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>", 44 "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
44 "<uses-permission android:name=\"android.permission.BLUETOOTH\"/>", 45 "<uses-permission android:name=\"android.permission.BLUETOOTH\"/>",
45 "<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>", 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 "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>" 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,6 +42,20 @@
42 <text class="error-text">{{ errorMsg }}</text> 42 <text class="error-text">{{ errorMsg }}</text>
43 </view> 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 <!-- Connected --> 59 <!-- Connected -->
46 <view v-if="connectedDevice" class="connected-card"> 60 <view v-if="connectedDevice" class="connected-card">
47 <view class="connected-header"> 61 <view class="connected-header">
@@ -153,26 +167,21 @@ import AppIcon from &#39;../../components/AppIcon.vue&#39; @@ -153,26 +167,21 @@ import AppIcon from &#39;../../components/AppIcon.vue&#39;
153 import SideMenu from '../../components/SideMenu.vue' 167 import SideMenu from '../../components/SideMenu.vue'
154 import LocationPicker from '../../components/LocationPicker.vue' 168 import LocationPicker from '../../components/LocationPicker.vue'
155 import { getStatusBarHeight } from '../../utils/statusBar' 169 import { getStatusBarHeight } from '../../utils/statusBar'
  170 +import classicBluetooth from '../../utils/print/bluetoothTool.js'
156 import { 171 import {
157 getPrinterType, 172 getPrinterType,
158 - getBluetoothConnection,  
159 - setBluetoothConnection,  
160 - setBuiltinPrinter,  
161 clearPrinter, 173 clearPrinter,
162 - sendToPrinter,  
163 type PrinterType, 174 type PrinterType,
164 } from '../../utils/print/printerConnection' 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 const statusBarHeight = getStatusBarHeight() 186 const statusBarHeight = getStatusBarHeight()
178 const isMenuOpen = ref(false) 187 const isMenuOpen = ref(false)
@@ -181,6 +190,17 @@ const connectingId = ref(&#39;&#39;) @@ -181,6 +190,17 @@ const connectingId = ref(&#39;&#39;)
181 const errorMsg = ref('') 190 const errorMsg = ref('')
182 const btAdapterReady = ref(false) 191 const btAdapterReady = ref(false)
183 const printerType = ref<PrinterType | ''>(getPrinterType() || 'bluetooth') 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 interface BtDevice { 205 interface BtDevice {
186 deviceId: string 206 deviceId: string
@@ -192,14 +212,26 @@ interface BtDevice { @@ -192,14 +212,26 @@ interface BtDevice {
192 const devices = ref<BtDevice[]>([]) 212 const devices = ref<BtDevice[]>([])
193 const discoveredIds = new Set<string>() 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 const connectedDevice = computed(() => { 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 return null 236 return null
205 }) 237 })
@@ -208,6 +240,7 @@ function switchType (type: &#39;bluetooth&#39; | &#39;builtin&#39;) { @@ -208,6 +240,7 @@ function switchType (type: &#39;bluetooth&#39; | &#39;builtin&#39;) {
208 printerType.value = type 240 printerType.value = type
209 if (type === 'bluetooth' && getPrinterType() === 'builtin') { 241 if (type === 'bluetooth' && getPrinterType() === 'builtin') {
210 clearPrinter() 242 clearPrinter()
  243 + refreshCurrentPrinter()
211 } 244 }
212 } 245 }
213 246
@@ -240,9 +273,16 @@ const startBleScan = () =&gt; { @@ -240,9 +273,16 @@ const startBleScan = () =&gt; {
240 success: () => { 273 success: () => {
241 isScanning.value = true 274 isScanning.value = true
242 errorMsg.value = '' 275 errorMsg.value = ''
  276 + debugInfo.value.lastBleEvent = 'scan running'
243 }, 277 },
244 fail: (err: any) => { 278 fail: (err: any) => {
245 console.error('BLE startBluetoothDevicesDiscovery fail:', err) 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 = () =&gt; { @@ -254,7 +294,7 @@ const stopDiscovery = () =&gt; {
254 }, 294 },
255 }) 295 })
256 // #ifdef APP-PLUS 296 // #ifdef APP-PLUS
257 - const classic = getClassicBluetooth() 297 + const classic = classicBluetooth
258 if (classic && classic.cancelClassicDiscovery) classic.cancelClassicDiscovery() 298 if (classic && classic.cancelClassicDiscovery) classic.cancelClassicDiscovery()
259 // #endif 299 // #endif
260 } 300 }
@@ -266,12 +306,12 @@ const onDeviceFound = (res: any) =&gt; { @@ -266,12 +306,12 @@ const onDeviceFound = (res: any) =&gt; {
266 const name = (d.localName || d.name || '').trim() 306 const name = (d.localName || d.name || '').trim()
267 const displayName = name || 'Unknown Device' 307 const displayName = name || 'Unknown Device'
268 discoveredIds.add(d.deviceId) 308 discoveredIds.add(d.deviceId)
269 - devices.value.push({ 309 + devices.value.push(describeDiscoveredPrinter({
270 deviceId: d.deviceId, 310 deviceId: d.deviceId,
271 name: displayName, 311 name: displayName,
272 RSSI: d.RSSI, 312 RSSI: d.RSSI,
273 type: 'ble', 313 type: 'ble',
274 - }) 314 + }))
275 } 315 }
276 devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) 316 devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100))
277 } 317 }
@@ -285,12 +325,12 @@ function mergeCachedBleDevices () { @@ -285,12 +325,12 @@ function mergeCachedBleDevices () {
285 if (discoveredIds.has(d.deviceId)) continue 325 if (discoveredIds.has(d.deviceId)) continue
286 const name = (d.localName || d.name || '').trim() 326 const name = (d.localName || d.name || '').trim()
287 discoveredIds.add(d.deviceId) 327 discoveredIds.add(d.deviceId)
288 - devices.value.push({ 328 + devices.value.push(describeDiscoveredPrinter({
289 deviceId: d.deviceId, 329 deviceId: d.deviceId,
290 name: name || 'Unknown Device', 330 name: name || 'Unknown Device',
291 RSSI: d.RSSI, 331 RSSI: d.RSSI,
292 type: 'ble', 332 type: 'ble',
293 - }) 333 + }))
294 } 334 }
295 if (list.length > 0) devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) 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,104 +339,98 @@ function mergeCachedBleDevices () {
299 339
300 function addPairedDevices () { 340 function addPairedDevices () {
301 // #ifdef APP-PLUS 341 // #ifdef APP-PLUS
302 - const classic = getClassicBluetooth() 342 + const classic = classicBluetooth
303 if (!classic || !classic.getPairedDevices) return 343 if (!classic || !classic.getPairedDevices) return
304 try { 344 try {
305 const paired = classic.getPairedDevices() 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 for (const p of paired) { 348 for (const p of paired) {
307 if (discoveredIds.has(p.deviceId)) continue 349 if (discoveredIds.has(p.deviceId)) continue
308 discoveredIds.add(p.deviceId) 350 discoveredIds.add(p.deviceId)
309 - devices.value.push({ 351 + devices.value.push(describeDiscoveredPrinter({
310 deviceId: p.deviceId, 352 deviceId: p.deviceId,
311 - name: p.name || 'Unknown', 353 + name: p.name || 'Unknown Device',
312 type: p.type || 'classic', 354 type: p.type || 'classic',
313 - }) 355 + }))
314 } 356 }
315 if (paired.length > 0) { 357 if (paired.length > 0) {
  358 + debugInfo.value.lastClassicEvent = 'paired devices loaded'
316 devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) 359 devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100))
  360 + } else {
  361 + debugInfo.value.lastClassicEvent = 'no paired devices'
317 } 362 }
318 } catch (e) { 363 } catch (e) {
319 console.error('addPairedDevices error:', e) 364 console.error('addPairedDevices error:', e)
  365 + debugInfo.value.lastClassicEvent = 'load paired devices failed'
320 } 366 }
321 // #endif 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 // #ifdef APP-PLUS 371 // #ifdef APP-PLUS
370 - const classic = getClassicBluetooth() 372 + const classic = classicBluetooth
371 if (classic && classic.startClassicDiscovery) { 373 if (classic && classic.startClassicDiscovery) {
372 try { 374 try {
373 classic.startClassicDiscovery( 375 classic.startClassicDiscovery(
374 (dev: { name: string; deviceId: string; type: string }) => { 376 (dev: { name: string; deviceId: string; type: string }) => {
  377 + debugInfo.value.lastClassicEvent = 'device found'
375 if (discoveredIds.has(dev.deviceId)) return 378 if (discoveredIds.has(dev.deviceId)) return
376 discoveredIds.add(dev.deviceId) 379 discoveredIds.add(dev.deviceId)
377 - devices.value.push({ 380 + devices.value.push(describeDiscoveredPrinter({
378 deviceId: dev.deviceId, 381 deviceId: dev.deviceId,
379 - name: dev.name || 'Unknown', 382 + name: dev.name || 'Unknown Device',
380 type: (dev.type as BtDevice['type']) || 'classic', 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 isScanning.value = true 391 isScanning.value = true
  392 + debugInfo.value.lastClassicEvent = 'scan running'
386 } catch (e) { 393 } catch (e) {
387 console.error('Classic discovery failed', e) 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 // #endif 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 try { 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 mergeCachedBleDevices() 434 mergeCachedBleDevices()
401 await new Promise<void>((resolve) => { 435 await new Promise<void>((resolve) => {
402 uni.getLocation({ 436 uni.getLocation({
@@ -424,77 +458,27 @@ const handleConnect = async (dev: BtDevice) =&gt; { @@ -424,77 +458,27 @@ const handleConnect = async (dev: BtDevice) =&gt; {
424 458
425 if (isScanning.value) stopDiscovery() 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 connectingId.value = '' 463 connectingId.value = ''
454 - errorMsg.value = 'Classic Bluetooth requires the app.'  
455 - // #endif 464 + errorMsg.value = permissionResult.message || 'Bluetooth permission denied.'
456 return 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 const handleUseBuiltin = () => { 479 const handleUseBuiltin = () => {
497 - setBuiltinPrinter() 480 + useBuiltinPrinter()
  481 + refreshCurrentPrinter()
498 uni.showToast({ title: 'Using built-in printer', icon: 'success' }) 482 uni.showToast({ title: 'Using built-in printer', icon: 'success' })
499 } 483 }
500 484
@@ -503,8 +487,7 @@ const handleTestPrint = async () =&gt; { @@ -503,8 +487,7 @@ const handleTestPrint = async () =&gt; {
503 if (testPrinting.value) return 487 if (testPrinting.value) return
504 testPrinting.value = true 488 testPrinting.value = true
505 try { 489 try {
506 - const data = buildTestTscLabel()  
507 - await sendToPrinter(data, (p) => { 490 + await testPrintCurrentPrinter((p) => {
508 if (p < 100) uni.showLoading({ title: `Printing ${p}%`, mask: true }) 491 if (p < 100) uni.showLoading({ title: `Printing ${p}%`, mask: true })
509 }) 492 })
510 uni.hideLoading() 493 uni.hideLoading()
@@ -531,30 +514,14 @@ const handleTestPrint = async () =&gt; { @@ -531,30 +514,14 @@ const handleTestPrint = async () =&gt; {
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 onMounted(() => { 523 onMounted(() => {
  524 + debugInfo.value.classicModuleReady = !!classicBluetooth
558 uni.onBluetoothDeviceFound(onDeviceFound) 525 uni.onBluetoothDeviceFound(onDeviceFound)
559 uni.onBluetoothAdapterStateChange((res: any) => { 526 uni.onBluetoothAdapterStateChange((res: any) => {
560 if (!res.available) { 527 if (!res.available) {
@@ -567,6 +534,7 @@ onMounted(() =&gt; { @@ -567,6 +534,7 @@ onMounted(() =&gt; {
567 } 534 }
568 }) 535 })
569 printerType.value = getPrinterType() || 'bluetooth' 536 printerType.value = getPrinterType() || 'bluetooth'
  537 + refreshCurrentPrinter()
570 }) 538 })
571 539
572 onUnmounted(() => { 540 onUnmounted(() => {
@@ -901,6 +869,37 @@ onUnmounted(() =&gt; { @@ -901,6 +869,37 @@ onUnmounted(() =&gt; {
901 gap: 12rpx; 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 .device-tag { 903 .device-tag {
905 display: inline-block; 904 display: inline-block;
906 font-size: 20rpx; 905 font-size: 20rpx;
美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
@@ -118,8 +118,7 @@ import SideMenu from &#39;../../components/SideMenu.vue&#39; @@ -118,8 +118,7 @@ import SideMenu from &#39;../../components/SideMenu.vue&#39;
118 import LocationPicker from '../../components/LocationPicker.vue' 118 import LocationPicker from '../../components/LocationPicker.vue'
119 import { getStatusBarHeight } from '../../utils/statusBar' 119 import { getStatusBarHeight } from '../../utils/statusBar'
120 import { generateNextLabelId } from '../../utils/printLog' 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 import chickenLabelImg from '../../static/chicken-lable.png' 122 import chickenLabelImg from '../../static/chicken-lable.png'
124 123
125 const statusBarHeight = getStatusBarHeight() 124 const statusBarHeight = getStatusBarHeight()
@@ -161,16 +160,9 @@ const displayProductName = computed(() =&gt; { @@ -161,16 +160,9 @@ const displayProductName = computed(() =&gt; {
161 }) 160 })
162 161
163 onShow(() => { 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 interface ProductData { 168 interface ProductData {
@@ -245,13 +237,12 @@ const handlePrint = async () =&gt; { @@ -245,13 +237,12 @@ const handlePrint = async () =&gt; {
245 } 237 }
246 isPrinting.value = true 238 isPrinting.value = true
247 try { 239 try {
248 - const data = buildTscLabel({ 240 + await printLabelForCurrentPrinter({
249 productName: displayProductName.value, 241 productName: displayProductName.value,
250 labelId: labelId.value, 242 labelId: labelId.value,
251 printQty: printQty.value, 243 printQty: printQty.value,
252 extraLine: lastEdited.value, 244 extraLine: lastEdited.value,
253 - })  
254 - await sendToPrinter(data, (percent) => { 245 + }, (percent) => {
255 if (percent >= 100) return 246 if (percent >= 100) return
256 uni.showLoading({ title: `Printing ${percent}%`, mask: true }) 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,6 +21,20 @@
21 <text class="badge-text">{{ t('printers.connected') }}</text> 21 <text class="badge-text">{{ t('printers.connected') }}</text>
22 </view> 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 <!-- Connected Device --> 38 <!-- Connected Device -->
25 <view v-if="currentType" class="printer-card connected"> 39 <view v-if="currentType" class="printer-card connected">
26 <view class="printer-icon"><AppIcon name="printer" size="md" color="white" /></view> 40 <view class="printer-icon"><AppIcon name="printer" size="md" color="white" /></view>
@@ -105,35 +119,51 @@ import { ref, onMounted, onUnmounted } from &#39;vue&#39; @@ -105,35 +119,51 @@ import { ref, onMounted, onUnmounted } from &#39;vue&#39;
105 import { useI18n } from 'vue-i18n' 119 import { useI18n } from 'vue-i18n'
106 import AppIcon from '../../components/AppIcon.vue' 120 import AppIcon from '../../components/AppIcon.vue'
107 import SideMenu from '../../components/SideMenu.vue' 121 import SideMenu from '../../components/SideMenu.vue'
  122 +import classicBluetooth from '../../utils/print/bluetoothTool.js'
  123 +import { ensureBluetoothPermissions } from '../../utils/print/bluetoothPermissions'
108 import { 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 const { t } = useI18n() 133 const { t } = useI18n()
124 const isMenuOpen = ref(false) 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 const isScanning = ref(false) 138 const isScanning = ref(false)
129 const devices = ref<any[]>([]) 139 const devices = ref<any[]>([])
130 const pairedDevices = ref<any[]>([]) 140 const pairedDevices = ref<any[]>([])
131 const isConnecting = ref(false) 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 let bleListenerRegistered = false 152 let bleListenerRegistered = false
133 153
134 const refreshStatus = () => { 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 const getTypeLabel = (type?: string) => { 169 const getTypeLabel = (type?: string) => {
@@ -145,64 +175,140 @@ const getTypeLabel = (type?: string) =&gt; { @@ -145,64 +175,140 @@ const getTypeLabel = (type?: string) =&gt; {
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 const addDeviceDedup = (device: any) => { 190 const addDeviceDedup = (device: any) => {
  191 + const described = describeDiscoveredPrinter(device)
149 const existing = devices.value.find(d => d.deviceId === device.deviceId) 192 const existing = devices.value.find(d => d.deviceId === device.deviceId)
150 if (!existing) { 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 // #ifdef APP-PLUS 200 // #ifdef APP-PLUS
156 const loadPairedDevices = () => { 201 const loadPairedDevices = () => {
157 try { 202 try {
158 const list = classicBluetooth.getPairedDevices() 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 } catch (e) { 211 } catch (e) {
161 console.error('Failed to load paired devices', e) 212 console.error('Failed to load paired devices', e)
162 pairedDevices.value = [] 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 // #endif 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 if (isScanning.value) return 237 if (isScanning.value) return
169 238
170 devices.value = [] 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 uni.openBluetoothAdapter({ 251 uni.openBluetoothAdapter({
190 success: () => { 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 if (!bleListenerRegistered) { 282 if (!bleListenerRegistered) {
192 bleListenerRegistered = true 283 bleListenerRegistered = true
193 uni.onBluetoothDeviceFound((res) => { 284 uni.onBluetoothDeviceFound((res) => {
194 res.devices.forEach(device => { 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 uni.startBluetoothDevicesDiscovery({ 294 uni.startBluetoothDevicesDiscovery({
202 allowDuplicatesKey: false, 295 allowDuplicatesKey: false,
203 - success: () => {}, 296 + success: () => {
  297 + debugInfo.value.lastBleEvent = 'scan running'
  298 + },
204 fail: (err) => { 299 fail: (err) => {
205 console.error('BLE startBluetoothDevicesDiscovery fail:', err) 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 = () =&gt; { @@ -217,6 +323,8 @@ const startScan = () =&gt; {
217 323
218 const stopScan = () => { 324 const stopScan = () => {
219 isScanning.value = false 325 isScanning.value = false
  326 + debugInfo.value.lastClassicEvent = 'stopped'
  327 + debugInfo.value.lastBleEvent = 'stopped'
220 uni.stopBluetoothDevicesDiscovery({ 328 uni.stopBluetoothDevicesDiscovery({
221 success: () => console.log('Stop BLE scan success'), 329 success: () => console.log('Stop BLE scan success'),
222 fail: (err) => console.error('Stop BLE scan fail', err) 330 fail: (err) => console.error('Stop BLE scan fail', err)
@@ -230,140 +338,56 @@ const stopScan = () =&gt; { @@ -230,140 +338,56 @@ const stopScan = () =&gt; {
230 // #endif 338 // #endif
231 } 339 }
232 340
233 -const connectBt = (device: any) => { 341 +const connectBt = async (device: any) => {
234 if (isConnecting.value) return 342 if (isConnecting.value) return
235 isConnecting.value = true 343 isConnecting.value = true
236 stopScan() 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 isConnecting.value = false 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 const connectBuiltin = () => { 367 const connectBuiltin = () => {
341 - setBuiltinPrinter()  
342 - currentType.value = 'builtin' 368 + useBuiltinPrinter()
  369 + refreshStatus()
343 uni.showToast({ title: t('printers.connectSuccess') }) 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 uni.showToast({ title: t('printers.disconnected') }) 376 uni.showToast({ title: t('printers.disconnected') })
360 } 377 }
361 378
362 const doTestPrint = async () => { 379 const doTestPrint = async () => {
363 try { 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 uni.showLoading({ title: t('labels.print.printing') }) 389 uni.showLoading({ title: t('labels.print.printing') })
366 - await sendToPrinter(data) 390 + await testPrintCurrentPrinter()
367 uni.hideLoading() 391 uni.hideLoading()
368 uni.showToast({ title: t('printers.testPrintSuccess') }) 392 uni.showToast({ title: t('printers.testPrintSuccess') })
369 } catch (e: any) { 393 } catch (e: any) {
@@ -393,6 +417,7 @@ const goBack = () =&gt; { @@ -393,6 +417,7 @@ const goBack = () =&gt; {
393 417
394 onMounted(() => { 418 onMounted(() => {
395 refreshStatus() 419 refreshStatus()
  420 + debugInfo.value.classicModuleReady = !!classicBluetooth
396 // #ifdef APP-PLUS 421 // #ifdef APP-PLUS
397 loadPairedDevices() 422 loadPairedDevices()
398 // #endif 423 // #endif
@@ -428,6 +453,11 @@ onUnmounted(() =&gt; { @@ -428,6 +453,11 @@ onUnmounted(() =&gt; {
428 .info-badge { display: flex; align-items: center; gap: 12rpx; margin-bottom: 32rpx; } 453 .info-badge { display: flex; align-items: center; gap: 12rpx; margin-bottom: 32rpx; }
429 .badge-num { font-size: 40rpx; font-weight: 700; color: var(--theme-primary); } 454 .badge-num { font-size: 40rpx; font-weight: 700; color: var(--theme-primary); }
430 .badge-text { font-size: 28rpx; color: #6b7280; } 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 .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; } 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 .printer-card.connected { background: var(--theme-primary); border: none; } 462 .printer-card.connected { background: var(--theme-primary); border: none; }
433 .printer-card.connected .printer-name, .printer-card.connected .printer-loc, .printer-card.connected .printer-status { color: #fff; } 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 // #ifdef APP-PLUS 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 try { 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 var blueToothTool = { 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 shortToast (msg) { 136 shortToast (msg) {
43 // #ifdef APP-PLUS 137 // #ifdef APP-PLUS
44 - showToast(msg) 138 + try {
  139 + if (activity) Toast.makeText(activity, String(msg), Toast.LENGTH_SHORT).show()
  140 + } catch (_) {}
45 // #endif 141 // #endif
46 }, 142 },
  143 + isSupportBluetooth () {
  144 + // #ifdef APP-PLUS
  145 + return btAdapter != null
  146 + // #endif
  147 + return false
  148 + },
47 getBluetoothStatus () { 149 getBluetoothStatus () {
48 // #ifdef APP-PLUS 150 // #ifdef APP-PLUS
49 - const btAdapter = getAdapter()  
50 return btAdapter != null && btAdapter.isEnabled() 151 return btAdapter != null && btAdapter.isEnabled()
51 // #endif 152 // #endif
52 return false 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 getPairedDevices () { 187 getPairedDevices () {
56 // #ifdef APP-PLUS 188 // #ifdef APP-PLUS
57 - const pairedDevices = [] 189 + let pairedDevices = []
58 try { 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 } catch (e) { 206 } catch (e) {
83 console.error('getPairedDevices error:', e) 207 console.error('getPairedDevices error:', e)
@@ -86,156 +210,360 @@ var blueToothTool = { @@ -86,156 +210,360 @@ var blueToothTool = {
86 // #endif 210 // #endif
87 return [] 211 return []
88 }, 212 },
89 - /** 经典蓝牙扫描(发现未配对设备,如 d320fax_295c)— 不过滤任何设备 */  
90 - startClassicDiscovery (onDeviceFound, onDiscoveryFinished) { 213 + discoveryNewDevice () {
91 // #ifdef APP-PLUS 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 btFindReceiver = null 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 btFindReceiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', { 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 if (BluetoothDevice.ACTION_FOUND === action) { 227 if (BluetoothDevice.ACTION_FOUND === action) {
108 - const device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) 228 + let device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
109 if (!device) return 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 if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED === action) { 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 filter.addAction(BluetoothDevice.ACTION_FOUND) 247 filter.addAction(BluetoothDevice.ACTION_FOUND)
130 filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) 248 filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
131 activity.registerReceiver(btFindReceiver, filter) 249 activity.registerReceiver(btFindReceiver, filter)
132 btAdapter.startDiscovery() 250 btAdapter.startDiscovery()
  251 + this.state.discoveryDeviceState = true
133 // #endif 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 // #ifdef APP-PLUS 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 // #endif 303 // #endif
146 }, 304 },
147 - /** 连接经典蓝牙设备(含 D320FAX Virtual BT Printer,支持 RFCOMM 回退) */  
148 connDevice (address, callback) { 305 connDevice (address, callback) {
149 // #ifdef APP-PLUS 306 // #ifdef APP-PLUS
  307 + this.cancelDiscovery()
150 if (btSocket != null) this.closeBtSocket() 308 if (btSocket != null) this.closeBtSocket()
151 this.state.readThreadState = false 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 this.shortToast('Bluetooth not available') 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 return false 320 return false
164 } 321 }
165 try { 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 return false 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 try { 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 } catch (e) { 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 return false 377 return false
196 } 378 }
197 return true 379 return true
198 // #endif 380 // #endif
199 - if (callback) callback(false) 381 + callback && callback(false)
200 return false 382 return false
201 }, 383 },
202 disConnDevice () { 384 disConnDevice () {
203 // #ifdef APP-PLUS 385 // #ifdef APP-PLUS
204 - this.closeBtSocket() 386 + if (btSocket != null) this.closeBtSocket()
205 this.state.readThreadState = false 387 this.state.readThreadState = false
206 - this.shortToast('Disconnected')  
207 // #endif 388 // #endif
208 }, 389 },
209 closeBtSocket () { 390 closeBtSocket () {
210 // #ifdef APP-PLUS 391 // #ifdef APP-PLUS
211 this.state.readThreadState = false 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 // #endif 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 // #ifdef APP-PLUS 475 // #ifdef APP-PLUS
222 if (!btOutStream) { 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 this.shortToast('Not connected') 493 this.shortToast('Not connected')
224 return false 494 return false
225 } 495 }
226 try { 496 try {
227 - const CHUNK_SIZE = 4096 497 + const CHUNK_SIZE = 512
  498 + this.state.lastSendMode = 'chunk-write'
  499 + this.state.lastSendError = ''
228 for (let i = 0; i < byteData.length; i += CHUNK_SIZE) { 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 return true 512 return true
233 } catch (e) { 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 return false 518 return false
235 } 519 }
236 // #endif 520 // #endif
237 return false 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 export default blueToothTool 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 * 打印机连接与下发:蓝牙(BLE) / 一体机(TCP localhost) 2 * 打印机连接与下发:蓝牙(BLE) / 一体机(TCP localhost)
3 */ 3 */
  4 +import type { ActiveBtDeviceType, PrinterType } from './types/printer'
  5 +import classicBluetooth from './bluetoothTool.js'
4 6
5 const STORAGE_PRINTER_TYPE = 'printerType' 7 const STORAGE_PRINTER_TYPE = 'printerType'
6 const STORAGE_BT_DEVICE_ID = 'btDeviceId' 8 const STORAGE_BT_DEVICE_ID = 'btDeviceId'
@@ -10,11 +12,10 @@ const STORAGE_BT_CHARACTERISTIC_ID = &#39;btCharacteristicId&#39; @@ -10,11 +12,10 @@ const STORAGE_BT_CHARACTERISTIC_ID = &#39;btCharacteristicId&#39;
10 const STORAGE_BT_DEVICE_TYPE = 'btDeviceType' // 'ble' | 'classic' 12 const STORAGE_BT_DEVICE_TYPE = 'btDeviceType' // 'ble' | 'classic'
11 const STORAGE_BLE_MTU = 'bleMTU' 13 const STORAGE_BLE_MTU = 'bleMTU'
12 const STORAGE_BUILTIN_PORT = 'builtinPort' 14 const STORAGE_BUILTIN_PORT = 'builtinPort'
  15 +const STORAGE_PRINTER_DRIVER_KEY = 'printerDriverKey'
13 16
14 const BUILTIN_PROBE_PORTS = [9100, 4000, 9000, 6000] 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 export const PrinterStorageKeys = { 20 export const PrinterStorageKeys = {
20 type: STORAGE_PRINTER_TYPE, 21 type: STORAGE_PRINTER_TYPE,
@@ -24,6 +25,7 @@ export const PrinterStorageKeys = { @@ -24,6 +25,7 @@ export const PrinterStorageKeys = {
24 btCharacteristicId: STORAGE_BT_CHARACTERISTIC_ID, 25 btCharacteristicId: STORAGE_BT_CHARACTERISTIC_ID,
25 btDeviceType: STORAGE_BT_DEVICE_TYPE, 26 btDeviceType: STORAGE_BT_DEVICE_TYPE,
26 bleMTU: STORAGE_BLE_MTU, 27 bleMTU: STORAGE_BLE_MTU,
  28 + driverKey: STORAGE_PRINTER_DRIVER_KEY,
27 } as const 29 } as const
28 30
29 export function setPrinterType (type: PrinterType) { 31 export function setPrinterType (type: PrinterType) {
@@ -37,6 +39,7 @@ export function setBluetoothConnection (info: { @@ -37,6 +39,7 @@ export function setBluetoothConnection (info: {
37 characteristicId?: string 39 characteristicId?: string
38 deviceType?: BtDeviceType 40 deviceType?: BtDeviceType
39 mtu?: number 41 mtu?: number
  42 + driverKey?: string
40 }) { 43 }) {
41 uni.setStorageSync(STORAGE_PRINTER_TYPE, 'bluetooth') 44 uni.setStorageSync(STORAGE_PRINTER_TYPE, 'bluetooth')
42 uni.setStorageSync(STORAGE_BT_DEVICE_ID, info.deviceId) 45 uni.setStorageSync(STORAGE_BT_DEVICE_ID, info.deviceId)
@@ -45,10 +48,12 @@ export function setBluetoothConnection (info: { @@ -45,10 +48,12 @@ export function setBluetoothConnection (info: {
45 uni.setStorageSync(STORAGE_BT_CHARACTERISTIC_ID, info.characteristicId || '') 48 uni.setStorageSync(STORAGE_BT_CHARACTERISTIC_ID, info.characteristicId || '')
46 uni.setStorageSync(STORAGE_BT_DEVICE_TYPE, info.deviceType || 'ble') 49 uni.setStorageSync(STORAGE_BT_DEVICE_TYPE, info.deviceType || 'ble')
47 uni.setStorageSync(STORAGE_BLE_MTU, info.mtu != null ? info.mtu : BLE_MTU_DEFAULT) 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 uni.setStorageSync(STORAGE_PRINTER_TYPE, 'builtin') 55 uni.setStorageSync(STORAGE_PRINTER_TYPE, 'builtin')
  56 + uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, driverKey)
52 } 57 }
53 58
54 export function clearPrinter () { 59 export function clearPrinter () {
@@ -60,6 +65,7 @@ export function clearPrinter () { @@ -60,6 +65,7 @@ export function clearPrinter () {
60 uni.removeStorageSync(STORAGE_BT_DEVICE_TYPE) 65 uni.removeStorageSync(STORAGE_BT_DEVICE_TYPE)
61 uni.removeStorageSync(STORAGE_BLE_MTU) 66 uni.removeStorageSync(STORAGE_BLE_MTU)
62 uni.removeStorageSync(STORAGE_BUILTIN_PORT) 67 uni.removeStorageSync(STORAGE_BUILTIN_PORT)
  68 + uni.removeStorageSync(STORAGE_PRINTER_DRIVER_KEY)
63 } 69 }
64 70
65 const BLE_MTU_DEFAULT = 20 71 const BLE_MTU_DEFAULT = 20
@@ -68,6 +74,10 @@ export function getPrinterType (): PrinterType | &#39;&#39; { @@ -68,6 +74,10 @@ export function getPrinterType (): PrinterType | &#39;&#39; {
68 return (uni.getStorageSync(STORAGE_PRINTER_TYPE) as PrinterType) || '' 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 export function getBluetoothConnection (): { 81 export function getBluetoothConnection (): {
72 deviceId: string 82 deviceId: string
73 deviceName: string 83 deviceName: string
@@ -186,20 +196,31 @@ function sendViaClassic ( @@ -186,20 +196,31 @@ function sendViaClassic (
186 } 196 }
187 return new Promise((resolve, reject) => { 197 return new Promise((resolve, reject) => {
188 try { 198 try {
189 - const classicBluetooth = (require('./bluetoothTool.js') as any).default  
190 if (!classicBluetooth) { 199 if (!classicBluetooth) {
191 reject(new Error('Classic Bluetooth not available')) 200 reject(new Error('Classic Bluetooth not available'))
192 return 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 const ok = classicBluetooth.sendByteData(sendData) 215 const ok = classicBluetooth.sendByteData(sendData)
200 if (onProgress) onProgress(100) 216 if (onProgress) onProgress(100)
201 if (ok) resolve() 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 } catch (e: any) { 224 } catch (e: any) {
204 reject(e) 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 +}