蓝牙热敏打印机使用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的逻辑也会导致残留数据留在缓冲区,引发后续打印乱码。
修复方案
- 分片发送+状态校验
不要一次性把整包图片数据写入输出流,按1KB大小分片发送,每发送一片后发送ESC v指令查询打印机缓冲区状态,等到打印机返回空闲状态再发送下一片,避免一次性塞满缓冲区。 - 降低图片数据量
你不需要把二维码放大到264px,大部分58mm热敏打印机的打印密度是8点/mm,3cm的二维码只需要240px足够。而且二维码是单色图形,用邻近插值缩放代替双线性插值,避免生成多余的灰度像素,可进一步降低转换后的1bit数据量。 - 修正连接关闭逻辑
发送完所有数据后不要立刻关闭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()
- 优化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

