Flutter蓝牙连接Phomemo M120热敏打印机无法打印问题排查
蓝牙打印Phomemo M120无内容排查方案
问题场景
通过Flutter应用使用flutter_blue_plus连接Phomemo M120热敏标签打印机,已成功建立连接、找到可写蓝牙特征,但使用esc_pos_utils生成的ESC/POS指令或自定义TSPL指令发送后,打印机仅吐出空白标签,无任何内容。厂商官方APP可正常打印,打印机硬件无故障。
排查步骤与代码优化建议
1. 确认指令集兼容性
Phomemo M120是标签专用打印机,默认采用TSPL指令集,而非通用小票打印机的ESC/POS。优先聚焦TSPL指令的调试,暂时放弃ESC/POS的尝试。
2. 修复TSPL指令的字节编码问题
当前TSPL代码使用command.codeUnits转换字节,这会采用UTF-16编码,而TSPL指令要求ASCII编码。修改转码逻辑:
import 'dart:convert'; List<int> printText(String text) { List<String> commands = []; // 设置标签尺寸(匹配实际耗材) commands.add('SIZE 40 mm,30 mm\r\n'); // 清除缓冲区 commands.add('CLS\r\n'); // 添加文本(使用打印机支持的字体) commands.add('TEXT 10,10,"0",0,1,1,"$text"\r\n'); // 打印1张标签 commands.add('PRINT 1\r\n'); // 改用ASCII编码转换指令 List<int> bytes = commands .map((command) => ascii.encode(command)) .expand((codeUnit) => codeUnit) .toList(); return bytes; }
注意:TSPL指令通常用\r\n作为换行符,而非单纯的\n,部分打印机对换行符格式敏感。
3. 确保蓝牙数据完整发送
- 检查并调整MTU值:MTU决定单次蓝牙传输的最大字节数,过小会导致指令截断。连接后主动请求更大的MTU:
await device.connect(); // 请求MTU(建议设为512,需打印机支持) await device.requestMtu(512); _mtu = await device.mtu.first; - 分批发送长指令:如果指令长度超过MTU,手动拆分并分批发送,每批发送后添加短暂延时:
List<int> bytes = printText("测试内容"); int chunkSize = _mtu - 3; // 预留蓝牙协议头空间 for (int i = 0; i < bytes.length; i += chunkSize) { int end = (i + chunkSize) > bytes.length ? bytes.length : i + chunkSize; await _characteristic!.write(bytes.sublist(i, end), allowLongWrite: false); await Future.delayed(Duration(milliseconds: 100)); }
4. 验证蓝牙特征是否正确
当前代码指定的特征UUID为0000ff02-0000-1000-8000-00805f9b34fb,但部分Phomemo打印机的打印特征是0000ff01-0000-1000-8000-00805f9b34fb。建议遍历所有可写特征,逐一测试:
for (BluetoothCharacteristic c in characteristics) { if (c.properties.write || c.properties.writeWithoutResponse) { // 标记所有可写特征,逐个测试打印 print("可写特征UUID: ${c.characteristicUuid}"); } }
5. 核对TSPL指令细节
- 移除多余的
END\n指令:TSPL无需该指令,PRINT 1已足够触发打印。 - 匹配实际标签尺寸:如果你的标签不是40mm×30mm,修改
SIZE指令参数为实际尺寸(可在厂商APP中查看)。 - 确认字体参数:
"3"可能不是打印机内置字体,改用"0"(默认字体)或"TSS24.BF2"这类官方文档标注的字体。
用户提供的原始代码
蓝牙连接代码
connectDevice(BluetoothDevice device) async { // listen for disconnection device.connectionState.listen((BluetoothConnectionState state) async { if (state == BluetoothConnectionState.disconnected) { // typically, start a periodic timer that tries to periodically reconnect. // Note: you must always re-discover services after disconnection! // TODO - do something } }); // Connect to the device // Note: You should always call `connectionState.listen` before you call connect! await device.connect(); _connectedDevice = device; // Yank the MTU _mtu = await device.mtu.first; // Note: You must call discoverServices after every connection! List<BluetoothService> services = await device.discoverServices(); _services = services; services.forEach((service) async { // Reads all characteristics var characteristics = service.characteristics; for (BluetoothCharacteristic c in characteristics) { print(c.toString()); if (c.characteristicUuid.toString() == "0000ff02-0000-1000-8000-00805f9b34fb" && service.serviceUuid.toString() == "0000ff00-0000-1000-8000-00805f9b34fb") { _characteristic = c; } // List<int> value = await c.read(); // print(value); } }); }
ESC/POS打印代码
final profile = await CapabilityProfile.load(); final generator = Generator(PaperSize.mm58, profile); List<int> bytes = []; bytes += generator.text("abc TEST 123"); bytes += generator.qrcode('example.com'); await _characteristic!.write(bytes, allowLongWrite: true);
TSPL打印代码
List<int> printText(String text) { List<String> commands = []; // Set fixed label size commands.add('SIZE 40 mm,30 mm\n'); // Clear the buffer commands.add('CLS\n'); // Add text, we assume standard text properties commands.add('TEXT 10,10,"3",0,1,1,"$text"\n'); // Print the label commands.add('PRINT 1\n'); // END commands.add('END\n'); // Convert commands to bytes List<int> bytes = commands .map((command) => command.codeUnits) .expand((codeUnit) => codeUnit) .toList(); return bytes; }
内容的提问来源于stack exchange,提问作者Peter Myers
相关产品推荐
相关产品推荐

