Commit ca4ab0f779b01bd40535f2a27f1c1493e720fd84
Merge branch 'main' of http://39.98.150.180/wangming/Food-Labeling-Management-Platform
Showing
10 changed files
with
271 additions
and
66 deletions
美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue
| ... | ... | @@ -163,7 +163,7 @@ |
| 163 | 163 | <text class="tips-item">2. On Android: enable Location (required for Bluetooth scan)</text> |
| 164 | 164 | <text class="tips-item">3. Place the printer within 10 m and tap Scan again</text> |
| 165 | 165 | <text class="tips-item">4. Devices with no name show as "Unknown Device"—you can still connect</text> |
| 166 | - <text class="tips-item">5. D320FAX (d320fax_295c): use Bluetooth mode, tap Scan—shows paired + nearby devices, no filtering</text> | |
| 166 | + <text class="tips-item">5. GP-D320FX (d320fx_xxxx): use Bluetooth mode, tap Scan—shows paired + nearby devices, no filtering</text> | |
| 167 | 167 | <text class="tips-item tips-item-last">6. Restart the printer or app if not visible</text> |
| 168 | 168 | </view> |
| 169 | 169 | </view> |
| ... | ... | @@ -260,8 +260,21 @@ async function refreshNativeDebug () { |
| 260 | 260 | function hasPreferredClassicDeviceInList () { |
| 261 | 261 | return devices.value.some((item: any) => { |
| 262 | 262 | const name = String(item?.name || '').toLowerCase() |
| 263 | - const type = String(item?.type || '').toLowerCase() | |
| 264 | - return name.includes('virtual bt printer') || type === 'classic' || type === 'dual' | |
| 263 | + const driverKey = String(item?.driverKey || '').toLowerCase() | |
| 264 | + return name.includes('virtual bt printer') || driverKey === 'd320fax' | |
| 265 | + }) | |
| 266 | +} | |
| 267 | + | |
| 268 | +function upsertDevice (device: any) { | |
| 269 | + const described = describeDiscoveredPrinter(device) | |
| 270 | + const existing = devices.value.find(item => item.deviceId === described.deviceId) | |
| 271 | + if (!existing) { | |
| 272 | + discoveredIds.add(described.deviceId) | |
| 273 | + devices.value.push(described) | |
| 274 | + return | |
| 275 | + } | |
| 276 | + Object.assign(existing, described, { | |
| 277 | + RSSI: described.RSSI != null ? described.RSSI : existing.RSSI, | |
| 265 | 278 | }) |
| 266 | 279 | } |
| 267 | 280 | |
| ... | ... | @@ -343,16 +356,13 @@ const stopDiscovery = () => { |
| 343 | 356 | const onDeviceFound = (res: any) => { |
| 344 | 357 | const foundDevices: any[] = res.devices || [] |
| 345 | 358 | for (const d of foundDevices) { |
| 346 | - if (discoveredIds.has(d.deviceId)) continue | |
| 347 | 359 | const name = (d.localName || d.name || '').trim() |
| 348 | - const displayName = name || 'Unknown Device' | |
| 349 | - discoveredIds.add(d.deviceId) | |
| 350 | - devices.value.push(describeDiscoveredPrinter({ | |
| 360 | + upsertDevice({ | |
| 351 | 361 | deviceId: d.deviceId, |
| 352 | - name: displayName, | |
| 362 | + name: name || 'Unknown Device', | |
| 353 | 363 | RSSI: d.RSSI, |
| 354 | 364 | type: 'ble', |
| 355 | - })) | |
| 365 | + }) | |
| 356 | 366 | } |
| 357 | 367 | devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) |
| 358 | 368 | } |
| ... | ... | @@ -363,15 +373,13 @@ function mergeCachedBleDevices () { |
| 363 | 373 | success: (res: any) => { |
| 364 | 374 | const list = res.devices || [] |
| 365 | 375 | for (const d of list) { |
| 366 | - if (discoveredIds.has(d.deviceId)) continue | |
| 367 | 376 | const name = (d.localName || d.name || '').trim() |
| 368 | - discoveredIds.add(d.deviceId) | |
| 369 | - devices.value.push(describeDiscoveredPrinter({ | |
| 377 | + upsertDevice({ | |
| 370 | 378 | deviceId: d.deviceId, |
| 371 | 379 | name: name || 'Unknown Device', |
| 372 | 380 | RSSI: d.RSSI, |
| 373 | 381 | type: 'ble', |
| 374 | - })) | |
| 382 | + }) | |
| 375 | 383 | } |
| 376 | 384 | if (list.length > 0) devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) |
| 377 | 385 | }, |
| ... | ... | @@ -387,13 +395,11 @@ function addPairedDevices () { |
| 387 | 395 | debugInfo.value.pairedCount = (paired || []).length |
| 388 | 396 | debugInfo.value.foundVirtualPrinter = (paired || []).some((item: any) => String(item?.name || '').toLowerCase().includes('virtual bt printer')) |
| 389 | 397 | for (const p of paired) { |
| 390 | - if (discoveredIds.has(p.deviceId)) continue | |
| 391 | - discoveredIds.add(p.deviceId) | |
| 392 | - devices.value.push(describeDiscoveredPrinter({ | |
| 398 | + upsertDevice({ | |
| 393 | 399 | deviceId: p.deviceId, |
| 394 | 400 | name: p.name || 'Unknown Device', |
| 395 | 401 | type: p.type || 'classic', |
| 396 | - })) | |
| 402 | + }) | |
| 397 | 403 | } |
| 398 | 404 | if (paired.length > 0) { |
| 399 | 405 | debugInfo.value.lastClassicEvent = 'paired devices loaded' |
| ... | ... | @@ -416,13 +422,11 @@ function startClassicScan () { |
| 416 | 422 | classic.startClassicDiscovery( |
| 417 | 423 | (dev: { name: string; deviceId: string; type: string }) => { |
| 418 | 424 | debugInfo.value.lastClassicEvent = 'device found' |
| 419 | - if (discoveredIds.has(dev.deviceId)) return | |
| 420 | - discoveredIds.add(dev.deviceId) | |
| 421 | - devices.value.push(describeDiscoveredPrinter({ | |
| 425 | + upsertDevice({ | |
| 422 | 426 | deviceId: dev.deviceId, |
| 423 | 427 | name: dev.name || 'Unknown Device', |
| 424 | 428 | type: (dev.type as BtDevice['type']) || 'classic', |
| 425 | - })) | |
| 429 | + }) | |
| 426 | 430 | devices.value.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)) |
| 427 | 431 | }, |
| 428 | 432 | () => { |
| ... | ... | @@ -542,7 +546,7 @@ const handleTestPrint = async () => { |
| 542 | 546 | if (msg === 'BUILTIN_PLUGIN_NOT_FOUND') { |
| 543 | 547 | uni.showModal({ |
| 544 | 548 | title: 'Use Bluetooth Mode', |
| 545 | - content: 'For GP-D320FAX, switch to Bluetooth mode and tap Scan to connect (e.g. d320fax_295c or Virtual BT Printer). Built-in mode needs custom app packaging.', | |
| 549 | + content: 'For GP-D320FX, switch to Bluetooth mode and tap Scan to connect (for example d320fx_xxxx). Built-in mode needs custom app packaging.', | |
| 546 | 550 | showCancel: false, |
| 547 | 551 | success: () => { switchType('bluetooth') }, |
| 548 | 552 | }) | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/more/printers.vue
| ... | ... | @@ -218,8 +218,8 @@ const normalizeDeviceName = (device: any) => { |
| 218 | 218 | const hasPreferredClassicDevice = () => { |
| 219 | 219 | return pairedDevices.value.some((item: any) => { |
| 220 | 220 | const name = String(item?.name || '').toLowerCase() |
| 221 | - const type = String(item?.type || '').toLowerCase() | |
| 222 | - return name.includes('virtual bt printer') || type === 'classic' || type === 'dual' | |
| 221 | + const driverKey = String(item?.driverKey || '').toLowerCase() | |
| 222 | + return name.includes('virtual bt printer') || driverKey === 'd320fax' | |
| 223 | 223 | }) |
| 224 | 224 | } |
| 225 | 225 | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/d320fax.ts
| ... | ... | @@ -3,7 +3,6 @@ import type { PrinterCandidate, PrinterDriver } from '../types/printer' |
| 3 | 3 | |
| 4 | 4 | const KEYWORDS = [ |
| 5 | 5 | 'd320fax', |
| 6 | - 'd320fx', | |
| 7 | 6 | 'virtual bt printer', |
| 8 | 7 | 'gp-d320fax', |
| 9 | 8 | ] |
| ... | ... | @@ -20,7 +19,7 @@ function score (device: PrinterCandidate): number { |
| 20 | 19 | export const d320faxDriver: PrinterDriver = { |
| 21 | 20 | key: 'd320fax', |
| 22 | 21 | brand: 'Gprinter', |
| 23 | - model: 'D320FAX/D320FX', | |
| 22 | + model: 'D320FAX', | |
| 24 | 23 | displayName: 'Gprinter D320FAX', |
| 25 | 24 | protocol: 'tsc', |
| 26 | 25 | preferredConnection: 'classic', | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/drivers/gpD320fx.ts
0 → 100644
| 1 | +import { buildTscLabelData, buildTscTestPrintData } from '../protocols/tscProtocol' | |
| 2 | +import type { PrinterCandidate, PrinterDriver } from '../types/printer' | |
| 3 | + | |
| 4 | +const KEYWORDS = [ | |
| 5 | + 'd320fx', | |
| 6 | + 'gp-d320fx', | |
| 7 | +] | |
| 8 | + | |
| 9 | +function score (device: PrinterCandidate): number { | |
| 10 | + const text = `${device.name || ''} ${device.deviceId || ''}`.toLowerCase() | |
| 11 | + let total = 0 | |
| 12 | + KEYWORDS.forEach((keyword) => { | |
| 13 | + if (text.includes(keyword)) total += 80 | |
| 14 | + }) | |
| 15 | + if (text.includes('gprinter')) total += 10 | |
| 16 | + return total | |
| 17 | +} | |
| 18 | + | |
| 19 | +export const gpD320fxDriver: PrinterDriver = { | |
| 20 | + key: 'gp-d320fx', | |
| 21 | + brand: 'Gprinter', | |
| 22 | + model: 'GP-D320FX', | |
| 23 | + displayName: 'Gprinter GP-D320FX', | |
| 24 | + protocol: 'tsc', | |
| 25 | + preferredConnection: 'ble', | |
| 26 | + preferredBleMtu: 512, | |
| 27 | + imageMaxWidthDots: 800, | |
| 28 | + imageDpi: 203, | |
| 29 | + keywords: KEYWORDS, | |
| 30 | + matches (device) { | |
| 31 | + return score(device) | |
| 32 | + }, | |
| 33 | + resolveConnectionType (device) { | |
| 34 | + return device.type === 'classic' ? 'classic' : 'ble' | |
| 35 | + }, | |
| 36 | + buildTestPrintData () { | |
| 37 | + return buildTscTestPrintData() | |
| 38 | + }, | |
| 39 | + buildLabelData (payload) { | |
| 40 | + return buildTscLabelData(payload) | |
| 41 | + }, | |
| 42 | +} | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/manager/driverRegistry.ts
| 1 | 1 | import { d320faxDriver } from '../drivers/d320fax' |
| 2 | 2 | import { genericTscDriver } from '../drivers/genericTsc' |
| 3 | +import { gpD320fxDriver } from '../drivers/gpD320fx' | |
| 3 | 4 | import { gpR3Driver } from '../drivers/gpR3' |
| 4 | 5 | import type { PrinterCandidate, PrinterDriver, ResolvedPrinterCandidate } from '../types/printer' |
| 5 | 6 | |
| 6 | 7 | const printerDrivers: PrinterDriver[] = [ |
| 7 | 8 | gpR3Driver, |
| 9 | + gpD320fxDriver, | |
| 8 | 10 | d320faxDriver, |
| 9 | 11 | genericTscDriver, |
| 10 | 12 | ] | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts
| ... | ... | @@ -41,7 +41,38 @@ function getPrinterTypeDisplayName (type: '' | 'bluetooth' | 'builtin'): string |
| 41 | 41 | function connectClassicBluetooth (device: PrinterCandidate, driver: PrinterDriver): Promise<void> { |
| 42 | 42 | return new Promise((resolve, reject) => { |
| 43 | 43 | // #ifdef APP-PLUS |
| 44 | - if (isNativeFastPrinterAvailable()) { | |
| 44 | + const shouldUseGenericClassicOnly = driver.key === 'gp-d320fx' | |
| 45 | + | |
| 46 | + const connectClassicSocketFallback = () => { | |
| 47 | + try { | |
| 48 | + if (!classicBluetooth || typeof classicBluetooth.connDevice !== 'function') { | |
| 49 | + reject(new Error('Classic Bluetooth fallback is not available.')) | |
| 50 | + return | |
| 51 | + } | |
| 52 | + classicBluetooth.connDevice(device.deviceId, (ok: boolean) => { | |
| 53 | + if (ok) { | |
| 54 | + setBluetoothConnection({ | |
| 55 | + deviceId: device.deviceId, | |
| 56 | + deviceName: device.name || 'Bluetooth Printer', | |
| 57 | + deviceType: 'classic', | |
| 58 | + transportMode: 'generic', | |
| 59 | + driverKey: driver.key, | |
| 60 | + mtu: driver.preferredBleMtu || 20, | |
| 61 | + }) | |
| 62 | + resolve() | |
| 63 | + return | |
| 64 | + } | |
| 65 | + const message = typeof classicBluetooth.getLastError === 'function' | |
| 66 | + ? classicBluetooth.getLastError() | |
| 67 | + : '' | |
| 68 | + reject(new Error(message || 'Classic Bluetooth connection failed.')) | |
| 69 | + }) | |
| 70 | + } catch (error: any) { | |
| 71 | + reject(error instanceof Error ? error : new Error(String(error || 'Classic Bluetooth connection failed.'))) | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 75 | + if (!shouldUseGenericClassicOnly && isNativeFastPrinterAvailable()) { | |
| 45 | 76 | connectNativeFastPrinterPlugin({ |
| 46 | 77 | deviceId: device.deviceId, |
| 47 | 78 | deviceName: device.name || 'Bluetooth Printer', |
| ... | ... | @@ -50,15 +81,24 @@ function connectClassicBluetooth (device: PrinterCandidate, driver: PrinterDrive |
| 50 | 81 | deviceId: device.deviceId, |
| 51 | 82 | deviceName: device.name || 'Bluetooth Printer', |
| 52 | 83 | deviceType: 'classic', |
| 84 | + transportMode: 'native-plugin', | |
| 53 | 85 | driverKey: driver.key, |
| 54 | 86 | mtu: driver.preferredBleMtu || 20, |
| 55 | 87 | }) |
| 56 | 88 | resolve() |
| 57 | 89 | }).catch((error: any) => { |
| 90 | + if (driver.key === 'd320fax') { | |
| 91 | + connectClassicSocketFallback() | |
| 92 | + return | |
| 93 | + } | |
| 58 | 94 | reject(error instanceof Error ? error : new Error(String(error || 'Classic Bluetooth connection failed.'))) |
| 59 | 95 | }) |
| 60 | 96 | return |
| 61 | 97 | } |
| 98 | + if (driver.key === 'd320fax' || shouldUseGenericClassicOnly) { | |
| 99 | + connectClassicSocketFallback() | |
| 100 | + return | |
| 101 | + } | |
| 62 | 102 | reject(new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND. Please rebuild the custom base with native-fast-printer.')) |
| 63 | 103 | // #endif |
| 64 | 104 | // #ifndef APP-PLUS |
| ... | ... | @@ -103,19 +143,44 @@ function findBleWriteCharacteristic (deviceId: string): Promise<{ serviceId: str |
| 103 | 143 | }) |
| 104 | 144 | } |
| 105 | 145 | |
| 146 | +function requestBleMtu (deviceId: string, preferredMtu: number): Promise<number> { | |
| 147 | + return new Promise((resolve) => { | |
| 148 | + const targetMtu = Math.max(20, Math.min(512, Math.round(preferredMtu || 20))) | |
| 149 | + if (targetMtu <= 20 || typeof (uni as any).setBLEMTU !== 'function') { | |
| 150 | + resolve(20) | |
| 151 | + return | |
| 152 | + } | |
| 153 | + let settled = false | |
| 154 | + const done = (value: number) => { | |
| 155 | + if (settled) return | |
| 156 | + settled = true | |
| 157 | + clearTimeout(timer) | |
| 158 | + resolve(Math.max(20, Math.round(value || 20))) | |
| 159 | + } | |
| 160 | + const timer = setTimeout(() => done(20), 3000) | |
| 161 | + ;(uni as any).setBLEMTU({ | |
| 162 | + deviceId, | |
| 163 | + mtu: targetMtu, | |
| 164 | + success: (res: any) => done(Number(res?.mtu) || targetMtu), | |
| 165 | + fail: () => done(20), | |
| 166 | + }) | |
| 167 | + }) | |
| 168 | +} | |
| 169 | + | |
| 106 | 170 | function connectBlePrinter (device: PrinterCandidate, driver: PrinterDriver): Promise<void> { |
| 107 | 171 | const finalizeExistingBleConnection = async () => { |
| 108 | 172 | const write = await findBleWriteCharacteristic(device.deviceId) |
| 109 | 173 | if (!write) { |
| 110 | 174 | throw new Error('No writable characteristic found. This device may not support printing.') |
| 111 | 175 | } |
| 176 | + const negotiatedMtu = await requestBleMtu(device.deviceId, driver.preferredBleMtu || 20) | |
| 112 | 177 | setBluetoothConnection({ |
| 113 | 178 | deviceId: device.deviceId, |
| 114 | 179 | deviceName: device.name || 'Bluetooth Printer', |
| 115 | 180 | serviceId: write.serviceId, |
| 116 | 181 | characteristicId: write.characteristicId, |
| 117 | 182 | deviceType: 'ble', |
| 118 | - mtu: driver.preferredBleMtu || 20, | |
| 183 | + mtu: negotiatedMtu, | |
| 119 | 184 | driverKey: driver.key, |
| 120 | 185 | }) |
| 121 | 186 | } |
| ... | ... | @@ -145,6 +210,14 @@ function connectBlePrinter (device: PrinterCandidate, driver: PrinterDriver): Pr |
| 145 | 210 | |
| 146 | 211 | export async function connectBluetoothPrinter (device: PrinterCandidate): Promise<PrinterDriver> { |
| 147 | 212 | const driver = resolvePrinterDriver(device) |
| 213 | + if (driver.key === 'gp-d320fx') { | |
| 214 | + try { | |
| 215 | + await connectBlePrinter(device, driver) | |
| 216 | + } catch (_) { | |
| 217 | + await connectClassicBluetooth(device, driver) | |
| 218 | + } | |
| 219 | + return driver | |
| 220 | + } | |
| 148 | 221 | const resolvedType = driver.resolveConnectionType(device) |
| 149 | 222 | if (resolvedType === 'classic') { |
| 150 | 223 | await connectClassicBluetooth(device, driver) |
| ... | ... | @@ -218,19 +291,25 @@ function canUseNativeFastTemplatePrint (driver: PrinterDriver): boolean { |
| 218 | 291 | const connection = getBluetoothConnection() |
| 219 | 292 | return driver.protocol === 'tsc' |
| 220 | 293 | && connection?.deviceType === 'classic' |
| 294 | + && connection?.transportMode === 'native-plugin' | |
| 221 | 295 | && isNativeFastPrinterAvailable() |
| 222 | 296 | } |
| 223 | 297 | |
| 224 | 298 | function getNativeClassicConnection () { |
| 225 | 299 | const connection = getBluetoothConnection() |
| 226 | - if (!connection || connection.deviceType !== 'classic') return null | |
| 300 | + if (!connection || connection.deviceType !== 'classic' || connection.transportMode !== 'native-plugin') return null | |
| 227 | 301 | return connection |
| 228 | 302 | } |
| 229 | 303 | |
| 230 | 304 | export async function testPrintCurrentPrinter (onProgress?: (percent: number) => void): Promise<PrinterDriver> { |
| 231 | 305 | const driver = getCurrentPrinterDriver() |
| 232 | 306 | const connection = getBluetoothConnection() |
| 233 | - if (driver.protocol === 'tsc' && connection?.deviceType === 'classic' && !isNativeFastPrinterAvailable()) { | |
| 307 | + if ( | |
| 308 | + driver.protocol === 'tsc' | |
| 309 | + && connection?.deviceType === 'classic' | |
| 310 | + && connection?.transportMode === 'native-plugin' | |
| 311 | + && !isNativeFastPrinterAvailable() | |
| 312 | + ) { | |
| 234 | 313 | throw new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND. Please rebuild the custom base with native-fast-printer.') |
| 235 | 314 | } |
| 236 | 315 | if (canUseNativeFastTemplatePrint(driver)) { |
| ... | ... | @@ -319,7 +398,12 @@ export async function printSystemTemplateForCurrentPrinter ( |
| 319 | 398 | ): Promise<PrinterDriver> { |
| 320 | 399 | const driver = getCurrentPrinterDriver() |
| 321 | 400 | const connection = getBluetoothConnection() |
| 322 | - if (driver.protocol === 'tsc' && connection?.deviceType === 'classic' && !isNativeFastPrinterAvailable()) { | |
| 401 | + if ( | |
| 402 | + driver.protocol === 'tsc' | |
| 403 | + && connection?.deviceType === 'classic' | |
| 404 | + && connection?.transportMode === 'native-plugin' | |
| 405 | + && !isNativeFastPrinterAvailable() | |
| 406 | + ) { | |
| 323 | 407 | throw new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND. Please rebuild the custom base with native-fast-printer.') |
| 324 | 408 | } |
| 325 | 409 | if (canUseNativeFastTemplatePrint(driver)) { |
| ... | ... | @@ -341,6 +425,7 @@ export async function printSystemTemplateForCurrentPrinter ( |
| 341 | 425 | const structuredTemplate = adaptSystemLabelTemplate(template, data, { |
| 342 | 426 | dpi: driver.imageDpi || 203, |
| 343 | 427 | printQty: options.printQty || 1, |
| 428 | + disableBitmapText: driver.key === 'gp-d320fx', | |
| 344 | 429 | }) |
| 345 | 430 | const bytes = driver.protocol === 'esc' |
| 346 | 431 | ? buildEscPosTemplateData(structuredTemplate) |
| ... | ... | @@ -360,7 +445,7 @@ export function disconnectCurrentPrinter (): Promise<void> { |
| 360 | 445 | |
| 361 | 446 | if (type === 'bluetooth' && connection?.deviceType === 'classic') { |
| 362 | 447 | // #ifdef APP-PLUS |
| 363 | - if (isNativeFastPrinterAvailable()) { | |
| 448 | + if (connection.transportMode === 'native-plugin' && isNativeFastPrinterAvailable()) { | |
| 364 | 449 | disconnectNativeFastPrinterPlugin().catch((e: any) => { |
| 365 | 450 | console.error('Disconnect native fast printer failed', e) |
| 366 | 451 | }).finally(() => { | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/nativeBitmapPatch.ts
| ... | ... | @@ -10,6 +10,14 @@ const DESIGN_DPI = 96 |
| 10 | 10 | const DEFAULT_THRESHOLD = 180 |
| 11 | 11 | const TEXT_PADDING_DOTS = 6 |
| 12 | 12 | |
| 13 | +function normalizePrinterLikeText (str: string): string { | |
| 14 | + return String(str || '') | |
| 15 | + .normalize('NFKC') | |
| 16 | + .replace(/[\u2018\u2019]/g, '\'') | |
| 17 | + .replace(/[\u201C\u201D]/g, '"') | |
| 18 | + .replace(/[\u2013\u2014]/g, '-') | |
| 19 | +} | |
| 20 | + | |
| 13 | 21 | type BitmapPatchItem = { |
| 14 | 22 | type: 'bitmap' |
| 15 | 23 | x: number |
| ... | ... | @@ -144,10 +152,11 @@ function splitTextLines (text: string, paint: any, maxWidth: number): string[] { |
| 144 | 152 | |
| 145 | 153 | export function shouldRasterizeTextElement (text: string, type: string): boolean { |
| 146 | 154 | const normalizedType = String(type || '').toUpperCase() |
| 147 | - if (!text) return false | |
| 148 | - if (normalizedType === 'TEXT_PRICE') return true | |
| 149 | - if (/[€£¥¥$éÉáàâäãåæçèêëìíîïñòóôöõøùúûüýÿœšž]/.test(text)) return true | |
| 150 | - return /[^\x20-\x7E]/.test(text) | |
| 155 | + const normalizedText = normalizePrinterLikeText(text) | |
| 156 | + if (!normalizedText) return false | |
| 157 | + if (normalizedType === 'TEXT_PRICE' && /[€£¥¥]/.test(normalizedText)) return true | |
| 158 | + if (/[€£¥¥éÉáàâäãåæçèêëìíîïñòóôöõøùúûüýÿœšž]/.test(normalizedText)) return true | |
| 159 | + return /[^\x20-\x7E]/.test(normalizedText) | |
| 151 | 160 | } |
| 152 | 161 | |
| 153 | 162 | export function createTextBitmapPatch (params: { | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/printerConnection.ts
| ... | ... | @@ -11,6 +11,7 @@ const STORAGE_BT_DEVICE_NAME = 'btDeviceName' |
| 11 | 11 | const STORAGE_BT_SERVICE_ID = 'btServiceId' |
| 12 | 12 | const STORAGE_BT_CHARACTERISTIC_ID = 'btCharacteristicId' |
| 13 | 13 | const STORAGE_BT_DEVICE_TYPE = 'btDeviceType' // 'ble' | 'classic' |
| 14 | +const STORAGE_BT_TRANSPORT_MODE = 'btTransportMode' // 'native-plugin' | 'generic' | |
| 14 | 15 | const STORAGE_BLE_MTU = 'bleMTU' |
| 15 | 16 | const STORAGE_BUILTIN_PORT = 'builtinPort' |
| 16 | 17 | const STORAGE_PRINTER_DRIVER_KEY = 'printerDriverKey' |
| ... | ... | @@ -29,6 +30,7 @@ export const PrinterStorageKeys = { |
| 29 | 30 | btServiceId: STORAGE_BT_SERVICE_ID, |
| 30 | 31 | btCharacteristicId: STORAGE_BT_CHARACTERISTIC_ID, |
| 31 | 32 | btDeviceType: STORAGE_BT_DEVICE_TYPE, |
| 33 | + btTransportMode: STORAGE_BT_TRANSPORT_MODE, | |
| 32 | 34 | bleMTU: STORAGE_BLE_MTU, |
| 33 | 35 | driverKey: STORAGE_PRINTER_DRIVER_KEY, |
| 34 | 36 | } as const |
| ... | ... | @@ -43,6 +45,7 @@ export function setBluetoothConnection (info: { |
| 43 | 45 | serviceId?: string |
| 44 | 46 | characteristicId?: string |
| 45 | 47 | deviceType?: BtDeviceType |
| 48 | + transportMode?: 'native-plugin' | 'generic' | |
| 46 | 49 | mtu?: number |
| 47 | 50 | driverKey?: string |
| 48 | 51 | }) { |
| ... | ... | @@ -52,6 +55,10 @@ export function setBluetoothConnection (info: { |
| 52 | 55 | uni.setStorageSync(STORAGE_BT_SERVICE_ID, info.serviceId || '') |
| 53 | 56 | uni.setStorageSync(STORAGE_BT_CHARACTERISTIC_ID, info.characteristicId || '') |
| 54 | 57 | uni.setStorageSync(STORAGE_BT_DEVICE_TYPE, info.deviceType || 'ble') |
| 58 | + uni.setStorageSync( | |
| 59 | + STORAGE_BT_TRANSPORT_MODE, | |
| 60 | + info.transportMode || (info.deviceType === 'classic' ? 'native-plugin' : 'generic') | |
| 61 | + ) | |
| 55 | 62 | uni.setStorageSync(STORAGE_BLE_MTU, info.mtu != null ? info.mtu : BLE_MTU_DEFAULT) |
| 56 | 63 | uni.setStorageSync(STORAGE_PRINTER_DRIVER_KEY, info.driverKey || '') |
| 57 | 64 | } |
| ... | ... | @@ -68,6 +75,7 @@ export function clearPrinter () { |
| 68 | 75 | uni.removeStorageSync(STORAGE_BT_SERVICE_ID) |
| 69 | 76 | uni.removeStorageSync(STORAGE_BT_CHARACTERISTIC_ID) |
| 70 | 77 | uni.removeStorageSync(STORAGE_BT_DEVICE_TYPE) |
| 78 | + uni.removeStorageSync(STORAGE_BT_TRANSPORT_MODE) | |
| 71 | 79 | uni.removeStorageSync(STORAGE_BLE_MTU) |
| 72 | 80 | uni.removeStorageSync(STORAGE_BUILTIN_PORT) |
| 73 | 81 | uni.removeStorageSync(STORAGE_PRINTER_DRIVER_KEY) |
| ... | ... | @@ -93,10 +101,12 @@ export function getBluetoothConnection (): { |
| 93 | 101 | serviceId: string |
| 94 | 102 | characteristicId: string |
| 95 | 103 | deviceType: BtDeviceType |
| 104 | + transportMode: 'native-plugin' | 'generic' | |
| 96 | 105 | mtu: number |
| 97 | 106 | } | null { |
| 98 | 107 | const deviceId = uni.getStorageSync(STORAGE_BT_DEVICE_ID) |
| 99 | 108 | const deviceType = (uni.getStorageSync(STORAGE_BT_DEVICE_TYPE) as BtDeviceType) || 'ble' |
| 109 | + const transportMode = (uni.getStorageSync(STORAGE_BT_TRANSPORT_MODE) as 'native-plugin' | 'generic') || 'generic' | |
| 100 | 110 | if (!deviceId) return null |
| 101 | 111 | if (deviceType === 'classic') { |
| 102 | 112 | return { |
| ... | ... | @@ -105,6 +115,7 @@ export function getBluetoothConnection (): { |
| 105 | 115 | serviceId: '', |
| 106 | 116 | characteristicId: '', |
| 107 | 117 | deviceType: 'classic', |
| 118 | + transportMode, | |
| 108 | 119 | mtu: BLE_MTU_DEFAULT, |
| 109 | 120 | } |
| 110 | 121 | } |
| ... | ... | @@ -117,6 +128,7 @@ export function getBluetoothConnection (): { |
| 117 | 128 | serviceId, |
| 118 | 129 | characteristicId, |
| 119 | 130 | deviceType: 'ble', |
| 131 | + transportMode, | |
| 120 | 132 | mtu: Number(uni.getStorageSync(STORAGE_BLE_MTU)) || BLE_MTU_DEFAULT, |
| 121 | 133 | } |
| 122 | 134 | } |
| ... | ... | @@ -209,15 +221,33 @@ function sendViaBle ( |
| 209 | 221 | return Promise.reject(new Error('Bluetooth printer not connected.')) |
| 210 | 222 | } |
| 211 | 223 | const { deviceId, serviceId, characteristicId, mtu } = conn |
| 224 | + const payloadSize = Math.max(20, Math.round((mtu || BLE_MTU_DEFAULT) > 23 ? (mtu || BLE_MTU_DEFAULT) - 3 : (mtu || BLE_MTU_DEFAULT))) | |
| 212 | 225 | const chunks: number[][] = [] |
| 213 | - for (let i = 0; i < data.length; i += mtu) { | |
| 214 | - chunks.push(data.slice(i, i + mtu)) | |
| 226 | + for (let i = 0; i < data.length; i += payloadSize) { | |
| 227 | + chunks.push(data.slice(i, i + payloadSize)) | |
| 215 | 228 | } |
| 216 | 229 | const total = chunks.length |
| 217 | 230 | let sent = 0 |
| 231 | + let completed = false | |
| 232 | + let timeoutId: ReturnType<typeof setTimeout> | null = setTimeout(() => {}, 0) | |
| 233 | + const writeDelayMs = payloadSize >= 180 ? 0 : (payloadSize > 20 ? 1 : 8) | |
| 234 | + | |
| 235 | + const resetTimeout = (reject: (reason?: any) => void) => { | |
| 236 | + if (timeoutId) clearTimeout(timeoutId) | |
| 237 | + timeoutId = setTimeout(() => { | |
| 238 | + if (completed) return | |
| 239 | + completed = true | |
| 240 | + reject(new Error('BLE write timeout')) | |
| 241 | + }, 15000) | |
| 242 | + } | |
| 218 | 243 | |
| 219 | 244 | function sendNext (): Promise<void> { |
| 245 | + if (completed) { | |
| 246 | + return Promise.reject(new Error('BLE write timeout')) | |
| 247 | + } | |
| 220 | 248 | if (sent >= total) { |
| 249 | + completed = true | |
| 250 | + if (timeoutId) clearTimeout(timeoutId) | |
| 221 | 251 | if (onProgress) onProgress(100) |
| 222 | 252 | return Promise.resolve() |
| 223 | 253 | } |
| ... | ... | @@ -228,17 +258,28 @@ function sendViaBle ( |
| 228 | 258 | view.setUint8(j, chunk[j] & 0xff) |
| 229 | 259 | } |
| 230 | 260 | return new Promise((resolve, reject) => { |
| 261 | + resetTimeout(reject) | |
| 231 | 262 | uni.writeBLECharacteristicValue({ |
| 232 | 263 | deviceId, |
| 233 | 264 | serviceId, |
| 234 | 265 | characteristicId, |
| 235 | 266 | value: buffer, |
| 236 | 267 | success: () => { |
| 268 | + if (completed) return | |
| 237 | 269 | sent++ |
| 238 | 270 | if (onProgress) onProgress(Math.round((sent / total) * 100)) |
| 239 | - setTimeout(() => sendNext().then(resolve).catch(reject), 10) | |
| 271 | + if (writeDelayMs <= 0) { | |
| 272 | + sendNext().then(resolve).catch(reject) | |
| 273 | + return | |
| 274 | + } | |
| 275 | + setTimeout(() => sendNext().then(resolve).catch(reject), writeDelayMs) | |
| 276 | + }, | |
| 277 | + fail: (err: any) => { | |
| 278 | + if (completed) return | |
| 279 | + completed = true | |
| 280 | + if (timeoutId) clearTimeout(timeoutId) | |
| 281 | + reject(new Error(err.errMsg || 'BLE write failed')) | |
| 240 | 282 | }, |
| 241 | - fail: (err: any) => reject(new Error(err.errMsg || 'BLE write failed')), | |
| 242 | 283 | }) |
| 243 | 284 | }) |
| 244 | 285 | } |
| ... | ... | @@ -256,9 +297,22 @@ function sendViaClassic ( |
| 256 | 297 | return Promise.reject(new Error('Classic Bluetooth printer not connected.')) |
| 257 | 298 | } |
| 258 | 299 | return new Promise((resolve, reject) => { |
| 300 | + let settled = false | |
| 301 | + const finish = (fn: () => void) => { | |
| 302 | + if (settled) return | |
| 303 | + settled = true | |
| 304 | + clearTimeout(timeoutId) | |
| 305 | + fn() | |
| 306 | + } | |
| 307 | + const timeoutId = setTimeout(() => { | |
| 308 | + finish(() => { | |
| 309 | + reject(buildClassicBluetoothError('Classic Bluetooth send timeout', conn.deviceId)) | |
| 310 | + }) | |
| 311 | + }, 15000) | |
| 312 | + | |
| 259 | 313 | try { |
| 260 | 314 | if (!classicBluetooth) { |
| 261 | - reject(new Error('Classic Bluetooth not available')) | |
| 315 | + finish(() => reject(new Error('Classic Bluetooth not available'))) | |
| 262 | 316 | return |
| 263 | 317 | } |
| 264 | 318 | const debugState = typeof classicBluetooth.getDebugState === 'function' |
| ... | ... | @@ -272,7 +326,7 @@ function sendViaClassic ( |
| 272 | 326 | const errorMessage = typeof classicBluetooth.getLastError === 'function' |
| 273 | 327 | ? classicBluetooth.getLastError() |
| 274 | 328 | : '' |
| 275 | - reject(buildClassicBluetoothError(errorMessage || 'Classic Bluetooth connection is not ready', conn.deviceId)) | |
| 329 | + finish(() => reject(buildClassicBluetoothError(errorMessage || 'Classic Bluetooth connection is not ready', conn.deviceId))) | |
| 276 | 330 | return |
| 277 | 331 | } |
| 278 | 332 | |
| ... | ... | @@ -283,31 +337,35 @@ function sendViaClassic ( |
| 283 | 337 | |
| 284 | 338 | if (typeof classicBluetooth.sendByteDataAsync === 'function') { |
| 285 | 339 | classicBluetooth.sendByteDataAsync(sendData, (ok: boolean, errorMessage?: string) => { |
| 286 | - if (onProgress) onProgress(100) | |
| 287 | - if (ok) { | |
| 288 | - resolve() | |
| 289 | - return | |
| 290 | - } | |
| 291 | - reject(buildClassicBluetoothError( | |
| 292 | - errorMessage || classicBluetooth.getLastError?.() || 'Classic Bluetooth send failed', | |
| 293 | - conn.deviceId | |
| 294 | - )) | |
| 340 | + finish(() => { | |
| 341 | + if (onProgress) onProgress(100) | |
| 342 | + if (ok) { | |
| 343 | + resolve() | |
| 344 | + return | |
| 345 | + } | |
| 346 | + reject(buildClassicBluetoothError( | |
| 347 | + errorMessage || classicBluetooth.getLastError?.() || 'Classic Bluetooth send failed', | |
| 348 | + conn.deviceId | |
| 349 | + )) | |
| 350 | + }) | |
| 295 | 351 | }) |
| 296 | 352 | return |
| 297 | 353 | } |
| 298 | 354 | |
| 299 | 355 | const ok = classicBluetooth.sendByteData(sendData) |
| 300 | - if (onProgress) onProgress(100) | |
| 301 | - if (ok) { | |
| 302 | - resolve() | |
| 303 | - return | |
| 304 | - } | |
| 305 | - const errorMessage = typeof classicBluetooth.getLastError === 'function' | |
| 306 | - ? classicBluetooth.getLastError() | |
| 307 | - : '' | |
| 308 | - reject(buildClassicBluetoothError(errorMessage || 'Classic Bluetooth send failed', conn.deviceId)) | |
| 356 | + finish(() => { | |
| 357 | + if (onProgress) onProgress(100) | |
| 358 | + if (ok) { | |
| 359 | + resolve() | |
| 360 | + return | |
| 361 | + } | |
| 362 | + const errorMessage = typeof classicBluetooth.getLastError === 'function' | |
| 363 | + ? classicBluetooth.getLastError() | |
| 364 | + : '' | |
| 365 | + reject(buildClassicBluetoothError(errorMessage || 'Classic Bluetooth send failed', conn.deviceId)) | |
| 366 | + }) | |
| 309 | 367 | } catch (e: any) { |
| 310 | - reject(buildClassicBluetoothError(e?.message || String(e || 'Classic Bluetooth send exception'), conn.deviceId)) | |
| 368 | + finish(() => reject(buildClassicBluetoothError(e?.message || String(e || 'Classic Bluetooth send exception'), conn.deviceId))) | |
| 311 | 369 | } |
| 312 | 370 | }) |
| 313 | 371 | // #endif | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts
| ... | ... | @@ -297,7 +297,10 @@ function buildTscTemplate ( |
| 297 | 297 | template: SystemLabelTemplate, |
| 298 | 298 | data: LabelTemplateData, |
| 299 | 299 | dpi: number, |
| 300 | - printQty: number | |
| 300 | + printQty: number, | |
| 301 | + options: { | |
| 302 | + disableBitmapText?: boolean | |
| 303 | + } = {} | |
| 301 | 304 | ): StructuredTscTemplate { |
| 302 | 305 | const widthMm = roundNumber(toMillimeter(template.width, template.unit || 'inch')) |
| 303 | 306 | const heightMm = roundNumber(toMillimeter(template.height, template.unit || 'inch')) |
| ... | ... | @@ -324,7 +327,7 @@ function buildTscTemplate ( |
| 324 | 327 | const scale = resolveTextScale(getConfigNumber(config, ['fontSize'], 14), dpi) |
| 325 | 328 | const align = resolveElementAlign(element, pageWidth) |
| 326 | 329 | |
| 327 | - if (shouldRasterizeTextElement(text, type)) { | |
| 330 | + if (!options.disableBitmapText && shouldRasterizeTextElement(text, type)) { | |
| 328 | 331 | const bitmapPatch = createTextBitmapPatch({ |
| 329 | 332 | element, |
| 330 | 333 | text, |
| ... | ... | @@ -510,13 +513,16 @@ export function adaptSystemLabelTemplate ( |
| 510 | 513 | options: { |
| 511 | 514 | dpi?: number |
| 512 | 515 | printQty?: number |
| 516 | + disableBitmapText?: boolean | |
| 513 | 517 | } = {} |
| 514 | 518 | ): StructuredLabelTemplate { |
| 515 | 519 | const dpi = options.dpi || 203 |
| 516 | 520 | const printQty = Math.max(1, Math.round(options.printQty || 1)) |
| 517 | 521 | return { |
| 518 | 522 | key: template.id || template.name || 'system-label-template', |
| 519 | - tsc: buildTscTemplate(template, data, dpi, printQty), | |
| 523 | + tsc: buildTscTemplate(template, data, dpi, printQty, { | |
| 524 | + disableBitmapText: options.disableBitmapText, | |
| 525 | + }), | |
| 520 | 526 | esc: buildEscTemplate(template, data, printQty), |
| 521 | 527 | } |
| 522 | 528 | } | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/tscLabelBuilder.ts
| ... | ... | @@ -220,7 +220,7 @@ export function buildTscTemplateLabel (template: StructuredTscTemplate): number[ |
| 220 | 220 | if (item.type === 'bitmap') { |
| 221 | 221 | const bytesPerRow = item.image.width / 8 |
| 222 | 222 | const bitmapBytes = pixelsToTscBitmapBytes(item.image) |
| 223 | - add(`BITMAP ${item.x},${item.y},${bytesPerRow},${item.image.height},0,`) | |
| 223 | + addCommandBytes(out, `BITMAP ${item.x},${item.y},${bytesPerRow},${item.image.height},0,`) | |
| 224 | 224 | for (let i = 0; i < bitmapBytes.length; i++) out.push(bitmapBytes[i]) |
| 225 | 225 | out.push(0x0d, 0x0a) |
| 226 | 226 | return |
| ... | ... | @@ -275,7 +275,7 @@ export function buildTscImageLabel ( |
| 275 | 275 | add('DENSITY 14') |
| 276 | 276 | add('SPEED 5') |
| 277 | 277 | add('CLS') |
| 278 | - add(`BITMAP ${x},${y},${bytesPerRow},${image.height},0,`) | |
| 278 | + addCommandBytes(out, `BITMAP ${x},${y},${bytesPerRow},${image.height},0,`) | |
| 279 | 279 | for (let i = 0; i < bitmapBytes.length; i++) out.push(bitmapBytes[i]) |
| 280 | 280 | out.push(0x0d, 0x0a) |
| 281 | 281 | add(`PRINT 1,${printQty}`) | ... | ... |