You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

蓝牙热敏打印机使用ESC/POS GS v 0指令打印图片时卡顿故障求助

蓝牙热敏打印机打印二维码图片卡顿、后续乱码问题排查

现有两台蓝牙热敏打印机及一台集成设备。其中一台打印机不支持通过GS ( k .. 49指令打印二维码,因此采用将file.bmp加载到Kotlin的Bitmap类中,再通过GS v 0指令作为图片发送的方式打印。
当前问题:打印二维码图片时,另一台打印机会在打印过程中卡顿,必须重启打印机才能恢复正常,否则后续会打印乱码。


基础参数

源文件参数

  • 82x82 像素
  • 打印尺寸2.9x2.9 cm(需求为3 cm)
  • 每像素24 bit
  • 2色

核心实现逻辑

1. Bitmap加载

加载原始图片的代码:

var bfo = BitmapFactory.Options()
bfo.outHeight = 20
bfo.outWidth = 20
bfo.inJustDecodeBounds = false

val fRawBmp = File(qrCodeRawFilePath)
val rawBmp = BitmapFactory.decodeFile(fRawBmp.absolutePath, bfo)

此处.outHeight和.outWidth参数对Bitmap尺寸没有生效,最终得到的rawBmp参数如下:

  • 82x82 px
  • 总字节数:26896
  • 每行字节数:328
  • 每像素字节数:4

2. 图片缩放

因为宽度过小,对图片进行等比缩放:

if(inBmp.width < 264) {
    val startTime = System.nanoTime()
    qrBmp = Bitmap.createScaledBitmap(inBmp, 264, 264, true)
    val endTime = System.nanoTime()
    val duration = endTime - startTime
    wasScaled = true
}

缩放后的Bitmap参数:

  • 264x264px
  • 总字节数278784
  • 每行字节数1056
  • 每像素字节数:4

3. GS v 0指令头构建

因为宽度是8的倍数无需补位,指令头构建逻辑:

val bytesPerLine = ceil((widthInPx.toFloat() / 8f).toDouble()).toInt()
val m  = 0 // 0-3
val xH = bytesPerLine / 256
val xL = bytesPerLine - xH * 256
val yH = heightInPx / 256
val yL = heightInPx - yH * 256
val imageBytes = ByteArray(8 + bytesPerLine * heightInPx)
System.arraycopy(byteArrayOf(0x1D, 0x76, 0x30, m.toByte(), xL.toByte(), xH.toByte(), yL.toByte(), yH.toByte()), 0, imageBytes, 0, 8)

4. 1bit图片转换

将图片转为每像素1bit格式,代码如下:

var i = 8
for (posY in 0 until heightInPx) {
    var jj = 0
    while (jj < widthInPx) {
        val stringBinary = StringBuilder()
        for (k in 0..7) {
            val posX = jj + k
            if (posX < widthInPx) {
                val color: Int = qrBmp.getPixel(posX, posY)
                val r = color shr 16 and 0xff
                val g = color shr 8 and 0xff
                val b = color and 0xff
                if (r > 160 && g > 160 && b > 160) {
                    stringBinary.append("0")
                } else {
                    stringBinary.append("1")
                }
            } else {
                stringBinary.append("0")
            }
        }
        imageBytes[i++] = stringBinary.toString().toInt(2).toByte()
        jj += 8
    }
}

最终指令参数

  • m: 0
  • xL: 33 bytes
  • xH: 0 bytes
  • yL: 8 dots
  • yH: 1 dots
  • k: 8712
  • 总数据长度: 8720 bytes (8+k)

将生成的字节流写入蓝牙socket的OutputStream后,打印机出现卡顿问题。


兼容性测试

已在多个不同Android版本、ABI、蓝牙版本和架构的设备上测试,仅在极少数设备上可正常打印,大部分场景下均失败。但使用第三方演示应用时,该打印机可以正常打印图片,初步推测是实现存在问题,可能为图片数据超出打印机缓冲区大小。


补充测试结果

按顺序发送text1 + 图片 + text2,每次写入后都flush流,可正常打印text1和图片,但text2不会被打印,测试代码如下:

bt.outStream!!.write(byteArrayOf(0x1B, 0x74, 0x02)) // ESC t codepage PC437 USA Standard Europe
bt.outStream?.write("text1\n".toByteArray(Charsets.ISO_8859_1))
br.outStream?.flush()

var bfo = BitmapFactory.Options()
bfo.inJustDecodeBounds = false
val fRawBmp = File(path2file)
val rawBmp = BitmapFactory.decodeFile(fRawBmp.absolutePath, bfo)
bt.outStream?.write(bmp2Bytes(rawBmp))
bt.outStream?.flush()

bt.outStream?.write("text2\n\n\n".toByteArray(Charsets.ISO_8859_1))
bt.outStream?.flush()
bt.outStream?.close()
bt.inStream?.close()
bt.socket?.close()

打印出的二维码可正常识别,但打印完成后仍需要重启打印机才能正常工作,进一步验证缓冲区溢出的推测。


问题排查与解决思路

根因定位

问题核心确实是打印机缓冲区溢出,民用热敏打印机的缓冲区普遍只有4KB~16KB,你单次发送8.7KB的图片数据已经接近或超过部分机型的缓冲区上限,加上蓝牙传输速率和打印机打印速率不匹配,就会导致数据丢包、缓冲区卡死。另外你最后直接关闭socket的逻辑也会导致残留数据留在缓冲区,引发后续打印乱码。

修复方案

  1. 分片发送+状态校验
    不要一次性把整包图片数据写入输出流,按1KB大小分片发送,每发送一片后发送ESC v指令查询打印机缓冲区状态,等到打印机返回空闲状态再发送下一片,避免一次性塞满缓冲区。
  2. 降低图片数据量
    你不需要把二维码放大到264px,大部分58mm热敏打印机的打印密度是8点/mm,3cm的二维码只需要240px足够。而且二维码是单色图形,用邻近插值缩放代替双线性插值,避免生成多余的灰度像素,可进一步降低转换后的1bit数据量。
  3. 修正连接关闭逻辑
    发送完所有数据后不要立刻关闭socket,先发送ESC @复位指令清空缓冲区,等待200ms让打印机处理完剩余数据再关闭连接,代码参考:
// 发送复位指令清空缓冲区残留数据
bt.outStream?.write(byteArrayOf(0x1B, 0x40))
bt.outStream?.flush()
// 等待打印机处理完成
Thread.sleep(200)
// 再关闭流和socket
bt.outStream?.close()
bt.inStream?.close()
bt.socket?.close()
  1. 优化1bit转换逻辑
    你当前用StringBuilder拼接二进制字符串的方式效率极低,还会产生额外内存开销,直接用位运算替换即可:
var b = 0
var bitCount = 0
for (posX in 0 until widthInPx) {
    val color = qrBmp.getPixel(posX, posY)
    val isBlack = ((color shr 16 and 0xff) <= 160 || (color shr 8 and 0xff) <=160 || (color and 0xff) <=160)
    b = (b shl 1) or if (isBlack) 1 else 0
    bitCount++
    if (bitCount == 8) {
        imageBytes[i++] = b.toByte()
        b = 0
        bitCount = 0
    }
}
if (bitCount > 0) {
    b = b shl (8 - bitCount)
    imageBytes[i++] = b.toByte()
}

内容的提问来源于stack exchange,提问作者vesperto

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.14 17:45:03