更新记录

1.0.0(2026-08-30)

  • 支持 ESC/POS、TSPL/TSPL2 打印指令编译。
  • 支持 Android、iOS BLE 与 TCP 打印。
  • 支持 uni-app x、uni-app Vue 2 和 Vue 3。
  • HarmonyOS 支持协议编译,暂不支持 BLE/TCP 传输。

平台兼容性

uni-app(5.24)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× × - 5.0 13 ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × - × ×

uni-app x(5.24)

Chrome Safari Android iOS 鸿蒙 微信小程序
× × 5.0 13 5.0.0 ×

hans-thermal-printer

hans-thermal-printer 是适用于 uni-app 和 uni-app x 的热敏打印插件。它可以把结构化任务编译为 ESC/POS 或 TSPL/TSPL2 字节,并通过 Android/iOS BLE 或 TCP 完成扫描、连接、排队和发送。协议编译 API 也可以独立使用。

安装与兼容性

  • 使用 HBuilderX 5.24+ 导入插件;付费授权版本需按 DCloud 规则通过云打包或自定义基座运行。
  • 当前只直接编码 ASCII。中文或其他字符应由调用方先编码为 raw 字节,或转换为单色 raster。
宿主 Android iOS HarmonyOS
uni-app x ESC/POS、TSPL、BLE、TCP ESC/POS、TSPL、BLE、TCP ESC/POS、TSPL;BLE/TCP 返回 9092011
uni-app Vue 2/3 ESC/POS、TSPL、BLE、TCP ESC/POS、TSPL、BLE、TCP 不支持

Web、小程序和快应用不支持。Android/iOS BLE + ESC/POS 已在 Xprinter XP-58IIQ 实际出纸;profile 和已验证机型不代表其他打印机型号、字库、切刀或标签定位一定兼容,接入新型号时仍应逐项实测。

导入 API 和类型

只从模块根目录导入,不要直接引用 utssdk 内部文件:

import {
  ThermalPrinter,
  compileEscPos,
  compileTspl,
  getProtocolCapabilities,
  printDataToHex,
  validateEscPos,
  validateTspl,
  EscPosCompileOptions,
  EscPosOperation,
  PrintCompileOutcome,
  PrintCompileResult,
  PrintDataToHexOutcome,
  PrintIssue,
  PrintValidationResult,
  ProtocolCapabilitiesOutcome,
  RasterData,
  ThermalPrinterError,
  ThermalPrinterStateSnapshot,
  ThermalPrinterTaskEvent,
  BleConnectOptions,
  BleScanDevice,
  BleScanOptions,
  PrinterSendOptions,
  TcpConnectOptions,
  TsplCompileOptions,
  TsplOperation,
} from '@/uni_modules/hans-thermal-printer'

常用公共类型:

类型 用途
EscPosCompileOptions / EscPosOperation ESC/POS 任务和操作
TsplCompileOptions / TsplOperation TSPL 标签任务和操作
RasterData 两种协议共用的单色位图
PrintCompileOutcome / PrintCompileResult 编译结果、字节和 warnings
PrintValidationResult / PrintIssue 参数校验结果
ThermalPrinterError 编译或查询失败信息
ThermalPrinterStateSnapshot / ThermalPrinterTaskEvent 连接状态和发送任务事件
BleScanDevice BLE 扫描结果
BleScanOptions / BleConnectOptions BLE 扫描、连接和写特征选择参数
TcpConnectOptions TCP host、port 和连接超时
PrinterSendOptions 发送超时、BLE 写模式、分包和间隔
PrintDataToHexOutcome 十六进制诊断结果
ProtocolCapabilitiesOutcome profile 能力查询结果

uni-app x 类型化调用

为了兼容 uni-app x 与传统 uni-app 的数组桥接,跨宿主函数使用动态签名。uni-app x 页面建议在调用边界把强类型 options 转为动态对象,并把返回值恢复为导出的 DTO:

function toApiOptions(value: any): UTSJSONObject {
  return JSON.parse<UTSJSONObject>(JSON.stringify(value) ?? '{}') as UTSJSONObject
}

function toApiBytes(values: number[]): Array<any> {
  const result: Array<any> = []
  let index = 0
  while (index < values.length) {
    result.push(values[index] as any)
    index++
  }
  return result
}

function asCompileOutcome(value: any): PrintCompileOutcome {
  const normalized = new UTSJSONObject(value)
  if (normalized['result'] != null) {
    const compiled = new UTSJSONObject(normalized['result'])
    if (compiled['warnings'] == null) compiled.set('warnings', [])
    normalized.set('result', compiled)
  }
  return JSON.parse<PrintCompileOutcome>(
    JSON.stringify(normalized) ?? '{"result":null,"error":null}',
  ) as PrintCompileOutcome
}

function asValidation(value: any): PrintValidationResult {
  const normalized = new UTSJSONObject(value)
  if (normalized['errors'] == null) normalized.set('errors', [])
  if (normalized['warnings'] == null) normalized.set('warnings', [])
  return JSON.parse<PrintValidationResult>(
    JSON.stringify(normalized) ?? '{"valid":false,"errors":[],"warnings":[]}',
  ) as PrintValidationResult
}

function asHexOutcome(value: any): PrintDataToHexOutcome {
  return JSON.parse<PrintDataToHexOutcome>(
    JSON.stringify(value) ?? '{"value":null,"error":null}',
  ) as PrintDataToHexOutcome
}

uni-app Vue 2/3 的 JavaScript 调用可直接读取返回对象,不需要上述 UTS 类型恢复函数。

权限与首次调用

  • Android 已声明网络、蓝牙扫描/连接权限,以及 Android 11 及以下系统所需的位置权限。首次扫描或连接会发起系统授权并返回 9092002,用户同意后应重新调用 startBleScan()connectBle()
  • iOS 已声明蓝牙和本地网络用途。首次使用 BLE 或局域网 TCP 时,系统可能弹出授权提示。
  • 扫描前应确认系统蓝牙已打开。Android 11 及以下系统还可能要求打开定位服务。
  • 权限被拒绝后不要循环请求;应提示用户前往系统设置授权。

编译 ESC/POS 小票

const operations: EscPosOperation[] = [
  { type: 'initialize' },
  { type: 'align', align: 'center' },
  { type: 'textStyle', bold: true, widthScale: 2, heightScale: 2 },
  { type: 'text', text: 'PICKUP 42' },
  { type: 'newLine' },
  { type: 'qrCode', text: 'PICKUP42', qrSize: 5, qrErrorLevel: 'M' },
  { type: 'feed', lines: 3 },
  { type: 'cut', cutMode: 'partial' },
]

const options: EscPosCompileOptions = {
  profileId: 'escpos-generic',
  operations: operations,
}

const validation: PrintValidationResult = asValidation(
  validateEscPos(toApiOptions(options)),
)
if (!validation.valid) {
  console.error(validation.errors[0].code, validation.errors[0].path)
}

const outcome: PrintCompileOutcome = asCompileOutcome(
  compileEscPos(toApiOptions(options)),
)
if (outcome.error != null) {
  const error: ThermalPrinterError = outcome.error
  console.error(error.errCode, error.errMsg, error.data?.path)
}

const receipt: PrintCompileResult | null = outcome.result
if (receipt != null) {
  const hex = asHexOutcome(printDataToHex(toApiBytes(receipt.data)))
  console.log(receipt.byteLength.toString(), hex.value ?? '')
  // 将 receipt.data 交给 BLE、TCP、USB 或串口传输层。
}

支持初始化、ASCII 文本、换行、走纸、对齐、粗体、下划线、字号、QR Model 2、Code 128、EAN-13、Code 39、单色 raster、全切/半切和 raw。

编译 TSPL 标签

const operations: TsplOperation[] = [
  { type: 'text', x: 2, y: 2, font: '0', text: 'PICKUP 42', xScale: 2, yScale: 2 },
  { type: 'box', x: 1, y: 1, width: 48, height: 28, thickness: 0.5 },
  { type: 'barcode', x: 5, y: 20, barcodeType: 'code128', text: '123456' },
]

const options: TsplCompileOptions = {
  width: 50,
  height: 30,
  unit: 'mm',
  dpi: 203,
  gap: 2,
  copies: 2,
  operations: operations,
}

const validation: PrintValidationResult = asValidation(
  validateTspl(toApiOptions(options)),
)
const outcome: PrintCompileOutcome = asCompileOutcome(
  compileTspl(toApiOptions(options)),
)

支持 SIZEGAP/BLINEDIRECTIONREFERENCESPEEDDENSITYCLSPRINT,以及 TEXTBARBOXQRCODEBARCODEBITMAP 和 raw。坐标单位支持 mm/dot,DPI 支持 203/300;毫米转点使用 round(mm * dpi / 25.4)

TSPL 默认拒绝可证明的越界。设置 allowOverflow: true 后会生成 CONTENT_OUT_OF_BOUNDS warning 并继续。文本、QR 和条码的完整尺寸依赖设备字体或编码结果,因此可能返回 UNVERIFIED_CONTENT_BOUNDS warning。

使用 RasterData

const logo: RasterData = {
  widthDots: 9,
  heightDots: 2,
  rowBytes: 2,
  data: [0xAA, 0x80, 0x55, 0x00],
}

const operation: EscPosOperation = {
  type: 'rasterImage',
  rasterImage: logo,
}

RasterData 使用行优先、MSB-first 格式,1 为黑点,0 为白点:

  • rowBytes 必须等于 ceil(widthDots / 8)
  • data.length 必须等于 rowBytes * heightDots
  • 每个字节必须是 0..255 的整数。
  • 每行末尾未使用的 padding 位必须为 0

插件不解码 PNG/JPEG/PDF,也不做缩放、阈值或抖动处理。

错误和 warnings

validateEscPos()validateTspl() 返回完整的 PrintValidationResult,用于在编译前展示全部 errors 和 warnings。

compileEscPos()compileTspl()printDataToHex() 和 capability 查询返回显式 outcome,不通过同步异常表示可预期的参数错误:

  • 成功值位于 resultvalue
  • 失败信息位于 error: ThermalPrinterError
  • 错误码范围为 9091xxx
  • error.data.issueCodeerror.data.path 提供结构化定位。
  • PrintCompileResult.warnings 保留不阻断输出的兼容性提示。

常见错误码:

错误码 含义与处理
9091xxx 协议、参数、编码、位图或输出大小错误;查看 error.data.issueCode/path
9092001 蓝牙未开启
9092002 权限未授予;授权后重新调用
9092006 配置的 BLE Service 不存在
9092007 没有可写特征
9092008 存在多个可写特征;显式填写 serviceIdwriteCharacteristicId
9092009 当前没有可用连接
9092010 TCP 连接失败;检查 host、port 和网络
9092011 当前平台不支持 BLE/TCP 传输
9093xxx 队列、发送、超时、取消或连接中断错误

查询 profile

const capabilities = JSON.parse<ProtocolCapabilitiesOutcome>(
  JSON.stringify(getProtocolCapabilities('escpos', 'escpos-generic'))
    ?? '{"result":null,"error":null}',
) as ProtocolCapabilitiesOutcome

console.log(capabilities.result?.paperWidthDots.toString() ?? '-')

内置 profile:

  • escpos-generic
  • tspl-generic-203
  • tspl-generic-300

profile 说明编译方言和参数上限,不代表具体打印机型号已经验证。

Raw、日志与输出限制

raw 只绕过命令语义校验,仍受总输出上限约束,并返回 RAW_BYPASSES_VALIDATION warning。不要把不受信任的输入直接作为 raw 命令。

日志默认关闭:

import { isLogEnabled, setLogEnabled } from '@/uni_modules/hans-thermal-printer'

setLogEnabled(true)
console.log(isLogEnabled().toString())

开启后,UTS 层会通过 console.log 记录 API、输出长度和传输事件;Android 原生 BLE 层会使用 Log.d 输出连接、服务发现、特征选择和每个分包的状态,tag 为 hans-thermal-printer。这些诊断日志只记录 UUID、状态、长度等元数据,不记录完整打印内容。

通过 HBuilderX CLI 运行 Android 项目时,可以用下面的命令查看同一份原生日志:

"<HX_CLI_PATH>" logcat app-android --project <project> --deviceId <adb-serial>

在终端中筛选 hans-thermal-printerBluetoothGatt,分别查看插件诊断和 Android GATT 回调。默认输出上限为 256 KiB,硬上限为 1 MiB,单任务最多包含 1024 个 operation。

传输 API

方法 用途
startBleScan() / stopBleScan() 开始或停止 BLE 扫描
connectBle() / connectTcp() 连接一台 BLE 或 TCP 打印机
sendData() 发送已经生成的原始字节
printEscPos() / printTspl() 编译任务并加入发送队列,立即返回 taskId
onStateChange() / offStateChange() 监听或移除实例状态监听器
onTaskEvent() / offTaskEvent() 监听或移除当前实例的任务监听器
getState() 获取当前连接、队列和任务快照
cancel() 取消指定任务
disconnect() / destroy() 断开连接;页面销毁时释放全部资源

状态事件对应 ThermalPrinterStateSnapshotstate 可能为 idle/scanning/connecting/discovering/ready/sending/disconnected/error/destroyed。任务事件对应 ThermalPrinterTaskEvent,依次产生 queued,可选的 compiling,然后是 sending,最终进入 written/failed/cancelled。动态回调可通过 JSON.stringify/JSON.parse<T> 恢复为导出的 DTO。

通过 TCP 连接和打印

import { ThermalPrinter } from '@/uni_modules/hans-thermal-printer'

const printer = new ThermalPrinter()

printer.onStateChange((event: UTSJSONObject) => {
  const state = JSON.parse<ThermalPrinterStateSnapshot>(
    JSON.stringify(event) ?? '{}',
  ) as ThermalPrinterStateSnapshot
  console.log('printer state', state.state)
})

printer.onTaskEvent((event: UTSJSONObject) => {
  const task = JSON.parse<ThermalPrinterTaskEvent>(
    JSON.stringify(event) ?? '{}',
  ) as ThermalPrinterTaskEvent
  console.log('task event', task.taskId.toString(), task.state)
})

const tcpOptions: TcpConnectOptions = {
  host: '192.168.1.80',
  port: 9100,
  connectTimeoutMs: 15000,
}

printer.connectTcp(toApiOptions(tcpOptions), (event: UTSJSONObject) => {
  if (event['state'] != 'ready') return
  printer.printEscPos({
    operations: [
      { type: 'initialize' },
      { type: 'text', text: 'PICKUP 42' },
      { type: 'newLine' },
      { type: 'feed', lines: 3 },
    ],
  }, { sendTimeoutMs: 30000 }, (task: UTSJSONObject) => {
    console.log('terminal task state', task['state'])
  })
})

TCP 没有通用默认端口,必须使用打印机说明或配置页给出的 host/port。Android 在字节写入并 flush 后、iOS 在 contentProcessed 后产生 written

通过 BLE 扫描和打印

const printer = new ThermalPrinter()

const scanOptions: BleScanOptions = { timeoutMs: 10000 }

printer.startBleScan(toApiOptions(scanOptions), (event: UTSJSONObject) => {
  const device = event['device'] as UTSJSONObject | null
  if (device == null) return
  const scanDevice = JSON.parse<BleScanDevice>(
    JSON.stringify(device) ?? '{}',
  ) as BleScanDevice

  printer.stopBleScan()
  const connectOptions: BleConnectOptions = {
    deviceId: scanDevice.deviceId,
    serviceId: '0000ffe0-0000-1000-8000-00805f9b34fb',
    writeCharacteristicId: '0000ffe2-0000-1000-8000-00805f9b34fb',
    autoDiscover: false,
    withoutResponse: false,
    segmentSize: 20,
    segmentInterval: 20,
  }
  printer.connectBle(toApiOptions(connectOptions), (state: UTSJSONObject) => {
    if (state['state'] != 'ready') return
    printer.sendData([0x1B, 0x40, 0x0A], { sendTimeoutMs: 30000 })
  })
})

写特征选择顺序为显式 UUID,然后才是自动发现。自动发现只有一个可写候选时成功;没有候选返回 9092007,多个候选返回 9092008segmentSize: 20segmentInterval: 20 只是保守起点,应按设备手册和实测调整。

iOS 扫描结果可能暂时没有名称,不要只按 name 过滤设备;优先使用打印机公开的广播 Service 过滤,并使用扫描结果中的 deviceId 连接。

队列、取消和完成语义

  • 每个 ThermalPrinter 实例只管理一个活动连接和一条 FIFO 队列;多台打印机应创建多个实例。
  • sendData()printEscPos()printTspl() 立即返回 taskId,任务 listener 接收 queued -> compiling/sending -> written/failed/cancelled
  • cancel(taskId) 可移除排队任务;发送中取消会关闭当前传输,结果标记 mayHavePrinted=true
  • 失败任务默认不重发。发送中失败可能已经被设备接收一部分,自动重试会造成重复出纸。
  • disconnect() 释放连接,destroy() 还会释放 listener、回调和原生资源。
  • written 只表示全部字节已被本地 BLE/TCP 传输层接受,不表示打印机已经出纸。

协议编译成功只证明生成了符合当前 profile 的字节;连接或 written 也不证明具体打印机的字库、切刀、二维码、条码、走纸精度或打印结果可用。这些能力仍需在目标机型上验证。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

Android 使用蓝牙扫描/连接权限和网络权限;iOS 使用蓝牙和本地网络权限

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件在本机处理用户选择的蓝牙设备标识、名称、信号强度、服务与特征 UUID,以及用户配置的打印机地址、端口和传输诊断信息;插件不主动上传这些数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。