Commit 961eecae3d8a442b5739a022bebd769929b22978

Authored by “wangming”
1 parent 0afa2a00

对打印机进行开发

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