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

Swift 5使用CoreBluetooth实现iOS向Phomemo M02蓝牙热敏打印机打印图片

Phomemo M02 iOS蓝牙打印故障修复方案

故障根因

  • 直接发送JPEG原始数据格式错误:Phomemo M02这类热敏打印机不支持直接解析JPEG格式,仅能识别符合ESC/POS指令集的二进制指令和处理后的二值化位图数据
  • 缺少打印控制指令:未发送初始化、打印触发、结束走纸的控制指令,导致打印机持续处于待接收状态,因此会无限出纸
  • 图片尺寸未适配打印机参数:M02的有效打印宽度为384像素,超过宽度的图片会被截断或解析错误,导致无图像输出

修复步骤

1. 确认打印机核心参数

Phomemo M02固定参数如下,所有打印逻辑需要适配参数:

  • 打印分辨率:203DPI
  • 单行有效打印像素:384px
  • 指令集:标准ESC/POS指令集

2. 调整打印逻辑

首先修改UIView截图逻辑,把截图缩放到适配打印机的尺寸,再做二值化处理转为打印机可识别的位图,最后按ESC/POS规范封装控制指令和位图数据再发送。
如果数据长度超过蓝牙MTU(默认20字节,协商后最大通常为512字节),需要拆分分包发送,避免丢包。

完整修正代码

extension UIView {
    // 生成适配打印机宽度的截图
    func captureShotForPrinter(targetWidth: CGFloat = 384) -> UIImage {
        // 生成原始截图
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, self.layer.contentsScale)
        drawHierarchy(in: self.bounds, afterScreenUpdates: true)
        let originalImage = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
        UIGraphicsEndImageContext()
        
        // 等比缩放到打印机适配宽度
        let scale = targetWidth / originalImage.size.width
        let targetHeight = originalImage.size.height * scale
        UIGraphicsBeginImageContext(CGSize(width: targetWidth, height: targetHeight))
        originalImage.draw(in: CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight))
        let scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
        UIGraphicsEndImageContext()
        return scaledImage
    }
    
    // 转为热敏打印机可识别的带指令打印数据
    func convertToThermalPrintData() -> Data {
        let image = self.captureShotForPrinter()
        guard let cgImage = image.cgImage else { return Data() }
        let width = cgImage.width
        let height = cgImage.height
        let bytesPerRow = (width + 7) / 8 // 每8个黑白像素占1字节
        var bitmapData = Data(count: bytesPerRow * height)
        
        // 图片二值化处理
        let grayColorSpace = CGColorSpaceCreateDeviceGray()
        guard let context = CGContext(
            data: &bitmapData,
            width: width,
            height: height,
            bitsPerComponent: 1,
            bytesPerRow: bytesPerRow,
            space: grayColorSpace,
            bitmapInfo: CGImageAlphaInfo.none.rawValue
        ) else { return Data() }
        context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
        
        // 封装ESC/POS打印指令
        var printData = Data()
        // 1. 初始化打印机指令 0x1B 0x40
        printData.append(Data([0x1B, 0x40]))
        // 2. 设置行间距为0 0x1B 0x33 0x00
        printData.append(Data([0x1B, 0x33, 0x00]))
        // 3. 位图打印指令头 0x1D 0x76 0x30 0x00
        printData.append(Data([0x1D, 0x76, 0x30, 0x00]))
        // 4. 写入位图宽高参数(小端序)
        let w1 = UInt8(bytesPerRow & 0xFF)
        let w2 = UInt8((bytesPerRow >> 8) & 0xFF)
        let h1 = UInt8(height & 0xFF)
        let h2 = UInt8((height >> 8) & 0xFF)
        printData.append(Data([w1, w2, h1, h2]))
        // 5. 写入二值化位图数据
        printData.append(bitmapData)
        // 6. 打印并走纸3行(调整最后一个数值可修改走纸长度)0x1B 0x64 0x03
        printData.append(Data([0x1B, 0x64, 0x03]))
        
        return printData
    }
}

发送打印数据逻辑:

let printData = printView.convertToThermalPrintData()
guard !printData.isEmpty else { return }
imageView.image = printView.captureShotForPrinter()

// 分包发送,适配蓝牙MTU限制
let mtu = globalPeripheral.maximumWriteValueLength(for: .withResponse)
var offset = 0
while offset < printData.count {
    let chunkLen = min(mtu, printData.count - offset)
    let chunk = printData.subdata(in: offset..<offset+chunkLen)
    globalPeripheral.writeValue(chunk, for: characteristic, type: .withResponse)
    offset += chunkLen
    // 每包间隔10ms避免丢包
    Thread.sleep(forTimeInterval: 0.01)
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 03:27:01