printerManager.ts 12.3 KB
import {
  clearPrinter,
  getBluetoothConnection,
  getCurrentPrinterDriverKey,
  getPrinterType,
  sendToPrinter,
  setBluetoothConnection,
  setBuiltinPrinter,
} from '../printerConnection'
import classicBluetooth from '../bluetoothTool.js'
import { rasterizeImageData, rasterizeImageForPrinter } from '../imageRaster'
import { buildEscPosImageData, buildEscPosTemplateData } from '../protocols/escPosBuilder'
import { buildTscImageData, buildTscTemplateData } from '../protocols/tscProtocol'
import {
  connectNativeFastPrinter as connectNativeFastPrinterPlugin,
  disconnectNativeFastPrinter as disconnectNativeFastPrinterPlugin,
  isNativeFastPrinterAvailable,
  printNativeFastTemplate as printNativeFastTemplatePlugin,
} from '../nativeFastPrinter'
import { adaptSystemLabelTemplate } from '../systemTemplateAdapter'
import { TEST_PRINT_SYSTEM_TEMPLATE, TEST_PRINT_TEMPLATE_DATA } from '../templates/testPrintTemplate'
import { describePrinterCandidate, getPrinterDriverByKey, resolvePrinterDriver } from './driverRegistry'
import type {
  CurrentPrinterSummary,
  LabelPrintPayload,
  LabelTemplateData,
  RawImageDataSource,
  PrintImageOptions,
  PrinterCandidate,
  PrinterDriver,
  StructuredLabelTemplate,
  SystemLabelTemplate,
} from '../types/printer'

function getPrinterTypeDisplayName (type: '' | 'bluetooth' | 'builtin'): string {
  if (type === 'bluetooth') return 'Bluetooth'
  if (type === 'builtin') return 'Built-in'
  return ''
}

function connectClassicBluetooth (device: PrinterCandidate, driver: PrinterDriver): Promise<void> {
  return new Promise((resolve, reject) => {
    // #ifdef APP-PLUS
    if (isNativeFastPrinterAvailable()) {
      connectNativeFastPrinterPlugin({
        deviceId: device.deviceId,
        deviceName: device.name || 'Bluetooth Printer',
      }).then(() => {
        setBluetoothConnection({
          deviceId: device.deviceId,
          deviceName: device.name || 'Bluetooth Printer',
          deviceType: 'classic',
          driverKey: driver.key,
          mtu: driver.preferredBleMtu || 20,
        })
        resolve()
      }).catch((error: any) => {
        reject(error instanceof Error ? error : new Error(String(error || 'Classic Bluetooth connection failed.')))
      })
      return
    }
    reject(new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND. Please rebuild the custom base with native-fast-printer.'))
    // #endif
    // #ifndef APP-PLUS
    reject(new Error('Classic Bluetooth requires the app.'))
    // #endif
  })
}

function findBleWriteCharacteristic (deviceId: string): Promise<{ serviceId: string; characteristicId: string } | null> {
  return new Promise((resolve) => {
    uni.getBLEDeviceServices({
      deviceId,
      success: (serviceRes) => {
        const services = serviceRes.services || []
        const next = (index: number) => {
          if (index >= services.length) {
            resolve(null)
            return
          }
          const serviceId = services[index].uuid
          uni.getBLEDeviceCharacteristics({
            deviceId,
            serviceId,
            success: (charRes) => {
              const target = (charRes.characteristics || []).find((item: any) => item.properties && item.properties.write)
              if (target) {
                resolve({
                  serviceId,
                  characteristicId: target.uuid,
                })
                return
              }
              next(index + 1)
            },
            fail: () => next(index + 1),
          })
        }
        next(0)
      },
      fail: () => resolve(null),
    })
  })
}

function connectBlePrinter (device: PrinterCandidate, driver: PrinterDriver): Promise<void> {
  const finalizeExistingBleConnection = async () => {
    const write = await findBleWriteCharacteristic(device.deviceId)
    if (!write) {
      throw new Error('No writable characteristic found. This device may not support printing.')
    }
    setBluetoothConnection({
      deviceId: device.deviceId,
      deviceName: device.name || 'Bluetooth Printer',
      serviceId: write.serviceId,
      characteristicId: write.characteristicId,
      deviceType: 'ble',
      mtu: driver.preferredBleMtu || 20,
      driverKey: driver.key,
    })
  }

  return new Promise((resolve, reject) => {
    uni.createBLEConnection({
      deviceId: device.deviceId,
      timeout: 10000,
      success: async () => {
        try {
          await finalizeExistingBleConnection()
          resolve()
        } catch (e: any) {
          reject(e)
        }
      },
      fail: (err: any) => {
        if (err?.errCode === -1) {
          finalizeExistingBleConnection().then(() => resolve()).catch(reject)
        } else {
          reject(new Error(err?.errMsg || 'BLE connection failed.'))
        }
      },
    })
  })
}

export async function connectBluetoothPrinter (device: PrinterCandidate): Promise<PrinterDriver> {
  const driver = resolvePrinterDriver(device)
  const resolvedType = driver.resolveConnectionType(device)
  if (resolvedType === 'classic') {
    await connectClassicBluetooth(device, driver)
  } else {
    await connectBlePrinter(device, driver)
  }
  return driver
}

export function useBuiltinPrinter (driverKey = 'generic-tsc') {
  setBuiltinPrinter(driverKey)
}

export function getCurrentPrinterDriver (): PrinterDriver {
  const type = getPrinterType()
  const storedKey = getCurrentPrinterDriverKey()
  if (storedKey) return getPrinterDriverByKey(storedKey)
  if (type === 'bluetooth') {
    const connection = getBluetoothConnection()
    if (connection) {
      return resolvePrinterDriver({
        deviceId: connection.deviceId,
        name: connection.deviceName,
        type: connection.deviceType,
      })
    }
  }
  return getPrinterDriverByKey('generic-tsc')
}

export function getCurrentPrinterSummary (): CurrentPrinterSummary {
  const type = getPrinterType()
  const driver = getCurrentPrinterDriver()
  if (type === 'builtin') {
    return {
      type,
      displayName: getPrinterTypeDisplayName(type),
      deviceId: 'builtin',
      driverKey: driver.key,
      driverName: driver.displayName,
      protocol: driver.protocol,
      deviceType: '',
    }
  }
  if (type === 'bluetooth') {
    const connection = getBluetoothConnection()
    if (connection) {
      return {
        type,
        displayName: getPrinterTypeDisplayName(type),
        deviceId: connection.deviceId,
        driverKey: driver.key,
        driverName: driver.displayName,
        protocol: driver.protocol,
        deviceType: connection.deviceType,
      }
    }
  }
  return {
    type: '',
    displayName: '',
    deviceId: '',
    driverKey: driver.key,
    driverName: driver.displayName,
    protocol: driver.protocol,
    deviceType: '',
  }
}

function canUseNativeFastTemplatePrint (driver: PrinterDriver): boolean {
  const connection = getBluetoothConnection()
  return driver.protocol === 'tsc'
    && connection?.deviceType === 'classic'
    && isNativeFastPrinterAvailable()
}

function getNativeClassicConnection () {
  const connection = getBluetoothConnection()
  if (!connection || connection.deviceType !== 'classic') return null
  return connection
}

export async function testPrintCurrentPrinter (onProgress?: (percent: number) => void): Promise<PrinterDriver> {
  const driver = getCurrentPrinterDriver()
  const connection = getBluetoothConnection()
  if (driver.protocol === 'tsc' && connection?.deviceType === 'classic' && !isNativeFastPrinterAvailable()) {
    throw new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND. Please rebuild the custom base with native-fast-printer.')
  }
  if (canUseNativeFastTemplatePrint(driver)) {
    const nativeConnection = getNativeClassicConnection()
    if (nativeConnection) {
      await printNativeFastTemplatePlugin({
        deviceId: nativeConnection.deviceId,
        deviceName: nativeConnection.deviceName,
        template: TEST_PRINT_SYSTEM_TEMPLATE,
        data: TEST_PRINT_TEMPLATE_DATA,
        dpi: driver.imageDpi || 203,
        printQty: 1,
      })
      if (onProgress) onProgress(100)
      return driver
    }
  }
  await sendToPrinter(driver.buildTestPrintData(), onProgress)
  return driver
}

export async function printLabelForCurrentPrinter (
  payload: LabelPrintPayload,
  onProgress?: (percent: number) => void
): Promise<PrinterDriver> {
  const driver = getCurrentPrinterDriver()
  await sendToPrinter(driver.buildLabelData(payload), onProgress)
  return driver
}

export async function printImageForCurrentPrinter (
  imageSource: string,
  options: PrintImageOptions = {},
  onProgress?: (percent: number) => void
): Promise<PrinterDriver> {
  const driver = getCurrentPrinterDriver()
  const raster = await rasterizeImageForPrinter(imageSource, driver, options)
  if (onProgress) onProgress(5)
  let data: number[] = []

  if (driver.protocol === 'esc') {
    data = buildEscPosImageData(raster, options)
  } else {
    data = buildTscImageData(raster, options, driver.imageDpi || 203)
  }

  await sendToPrinter(data, onProgress)
  return driver
}

export async function printImageDataForCurrentPrinter (
  imageData: RawImageDataSource,
  options: PrintImageOptions = {},
  onProgress?: (percent: number) => void
): Promise<PrinterDriver> {
  const driver = getCurrentPrinterDriver()
  const raster = rasterizeImageData(imageData, options)
  if (onProgress) onProgress(5)
  const data = driver.protocol === 'esc'
    ? buildEscPosImageData(raster, options)
    : buildTscImageData(raster, options, driver.imageDpi || 203)
  await sendToPrinter(data, onProgress)
  return driver
}

export async function printTemplateForCurrentPrinter (
  template: StructuredLabelTemplate,
  data: LabelTemplateData = {},
  onProgress?: (percent: number) => void
): Promise<PrinterDriver> {
  const driver = getCurrentPrinterDriver()
  const bytes = driver.protocol === 'esc'
    ? buildEscPosTemplateData(template, data)
    : buildTscTemplateData(template, data)
  await sendToPrinter(bytes, onProgress)
  return driver
}

export async function printSystemTemplateForCurrentPrinter (
  template: SystemLabelTemplate,
  data: LabelTemplateData = {},
  options: {
    printQty?: number
  } = {},
  onProgress?: (percent: number) => void
): Promise<PrinterDriver> {
  const driver = getCurrentPrinterDriver()
  const connection = getBluetoothConnection()
  if (driver.protocol === 'tsc' && connection?.deviceType === 'classic' && !isNativeFastPrinterAvailable()) {
    throw new Error('NATIVE_FAST_PRINTER_PLUGIN_NOT_FOUND. Please rebuild the custom base with native-fast-printer.')
  }
  if (canUseNativeFastTemplatePrint(driver)) {
    const nativeConnection = getNativeClassicConnection()
    if (nativeConnection) {
      await printNativeFastTemplatePlugin({
        deviceId: nativeConnection.deviceId,
        deviceName: nativeConnection.deviceName,
        template,
        data,
        dpi: driver.imageDpi || 203,
        printQty: options.printQty || 1,
      })
      if (onProgress) onProgress(100)
      return driver
    }
  }

  const structuredTemplate = adaptSystemLabelTemplate(template, data, {
    dpi: driver.imageDpi || 203,
    printQty: options.printQty || 1,
  })
  const bytes = driver.protocol === 'esc'
    ? buildEscPosTemplateData(structuredTemplate)
    : buildTscTemplateData(structuredTemplate)
  await sendToPrinter(bytes, onProgress)
  return driver
}

export function describeDiscoveredPrinter (device: PrinterCandidate) {
  return describePrinterCandidate(device)
}

export function disconnectCurrentPrinter (): Promise<void> {
  return new Promise((resolve) => {
    const type = getPrinterType()
    const connection = getBluetoothConnection()

    if (type === 'bluetooth' && connection?.deviceType === 'classic') {
      // #ifdef APP-PLUS
      if (isNativeFastPrinterAvailable()) {
        disconnectNativeFastPrinterPlugin().catch((e: any) => {
          console.error('Disconnect native fast printer failed', e)
        }).finally(() => {
          clearPrinter()
          resolve()
        })
        return
      }
      try {
        const classic = classicBluetooth
        if (classic && classic.disConnDevice) classic.disConnDevice()
      } catch (e) {
        console.error('Disconnect classic bluetooth failed', e)
      }
      // #endif
      clearPrinter()
      resolve()
      return
    }

    clearPrinter()
    if (type === 'bluetooth' && connection?.deviceId) {
      uni.closeBLEConnection({
        deviceId: connection.deviceId,
        complete: () => resolve(),
      })
      return
    }
    resolve()
  })
}