如何将Android应用Activity与POS打印机连接并自定义账单格式
问题描述
我正在给现有应用做更新,要集成到手持移动POS机上,实现账单的纸质打印功能。之前应用只支持PDF和Excel格式的账单分享,现在客户要求提供纸质账单。
我写了一个Kotlin类用于蓝牙连接POS打印机,但目前应用还是连不上POS机;另外我想知道怎么自定义账单格式,我知道可以通过BluetoothConnect类里的printText()函数来做,但不清楚具体操作,这是不是和打印机支持的ESC/POS命令有关?
现有代码
BluetoothConnect 类
import android.Manifest import android.app.Activity import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothSocket import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.util.Log import androidx.core.app.ActivityCompat import com.spoonerpt.rest_services.WebUtils import java.io.OutputStream import java.util.UUID class BluetoothConnect( private val context: Context, private val activity: Activity ) { private val bluetoothAdapter: BluetoothAdapter = BluetoothAdapter.getDefaultAdapter() private lateinit var bluetoothSocket: BluetoothSocket private lateinit var outputStream: OutputStream private val PERMISSIONS = arrayOf( Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN ) private fun requestPermissions() { ActivityCompat.requestPermissions(activity, PERMISSIONS, WebUtils.REQUEST_PERMISSION_BLUETOOTH) } fun checkPermissionsAtRuntime(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return true } else { for (permission in PERMISSIONS) { if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED ) { return false } } } return true } fun connect() { val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice("00:11:22:33:44:55") // Replace with your printer's MAC address try { if (ActivityCompat.checkSelfPermission( context, Manifest.permission.BLUETOOTH_CONNECT ) != PackageManager.PERMISSION_GRANTED ) { // Permissions not granted, request permissions requestPermissions() return } // Fetch UUIDs if not available if (device.uuids == null) { device.fetchUuidsWithSdp() device.uuids?.let { val uuid: UUID = it[0].uuid bluetoothSocket = device.createRfcommSocketToServiceRecord(uuid) bluetoothSocket.connect() outputStream = bluetoothSocket.outputStream } ?: run { Log.e("PrintManager", "UUIDs not found") } } else { val uuid: UUID = device.uuids[0].uuid bluetoothSocket = device.createRfcommSocketToServiceRecord(uuid) bluetoothSocket.connect() outputStream = bluetoothSocket.outputStream } } catch (e: Exception) { Log.e("PrintManager", "Error while connecting to Bluetooth device", e) } } fun printText(text: String) { outputStream.write(text.toByteArray()) } fun disconnect() { outputStream.close() bluetoothSocket.close() } }
Activity 中的实现代码
// this is initialized above oncreate method. private lateinit var billInfo : StringBuilder // billInfo is used to enter the details from the server. //this generateBill function is the structure for the bill (Bill Format). fun generateBill(){ val billDetails = StringBuilder() billDetails.append("Saras Bill Details\n") billDetails.append(billInfo) printBill(billDetails.toString()) } //printBill is the function where we connect to the print roll of the handheld mobile machine. fun printBill(billDetails:String){ printermanager = BluetoothConnect(this,this) if(printermanager.checkPermissionsAtRuntime()){ printermanager.connect() printermanager.printText(billDetails) printermanager.disconnect()} else{ printermanager.requestPermissions() } } //(this,this) is the context and activity reference for the Bluetooth Connect class. // here is the code on how the billInfo variable is collecting the data from the server.. rcdfAssismentresponse = dataItem.rcdfAssismentitem as java.util.ArrayList<RCDFAssismentItem> billInfo.append("\n${dataItem.rcdfAssismentitem.get(0).ItemType}\n") billInfo.append("\n${dataItem.rcdfAssismentitem.get(0).UnitCode}\n") billInfo.append("\n${dataItem.rcdfAssismentitem.get(0).QtyLTR}\n") billInfo.append("\n${dataItem.rcdfAssismentitem.get(0).Rate}\n")
解决方案
连接问题排查和修复
1. 核心问题修复
- MAC地址验证:将代码中硬编码的
00:11:22:33:44:55替换为实际POS打印机的蓝牙MAC地址(可在打印机设置或手机蓝牙配对列表中查看)。 - UUID异步问题:
fetchUuidsWithSdp()是异步操作,原代码直接调用后获取UUID会大概率失败,改用蓝牙串口服务的标准SPP UUID:00001101-0000-1000-8000-00805F9B34FB,绝大多数POS打印机都支持。 - 权限逻辑修正:Android 13+(TIRAMISU)仍需检查
BLUETOOTH_CONNECT权限,原checkPermissionsAtRuntime()直接返回true的逻辑错误,需补充权限检查。 - 权限回调处理:权限请求后,必须在Activity的
onRequestPermissionsResult中处理授权结果,授权成功后再执行打印,而非请求后直接返回。
2. 优化后的连接代码
private val SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") fun connect() { // 检查蓝牙是否开启 if (!bluetoothAdapter.isEnabled) { Log.e("PrintManager", "蓝牙未开启") return } val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice("实际打印机MAC地址") try { if (ActivityCompat.checkSelfPermission( context, Manifest.permission.BLUETOOTH_CONNECT ) != PackageManager.PERMISSION_GRANTED ) { requestPermissions() return } // 使用标准SPP UUID连接 bluetoothSocket = device.createRfcommSocketToServiceRecord(SPP_UUID) bluetoothAdapter.cancelDiscovery() // 取消扫描避免干扰连接 bluetoothSocket.connect() outputStream = bluetoothSocket.outputStream Log.d("PrintManager", "打印机连接成功") } catch (e: Exception) { Log.e("PrintManager", "蓝牙连接失败", e) // 兼容部分不支持标准UUID的设备 try { val method = device.javaClass.getMethod("createRfcommSocket", Int::class.javaPrimitiveType) bluetoothSocket = method.invoke(device, 1) as BluetoothSocket bluetoothSocket.connect() outputStream = bluetoothSocket.outputStream Log.d("PrintManager", "通过兼容方式连接成功") } catch (fallbackE: Exception) { Log.e("PrintManager", "兼容连接也失败", fallbackE) } } }
3. Activity权限回调补充
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == WebUtils.REQUEST_PERMISSION_BLUETOOTH) { if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) { // 权限授予成功,重新执行打印 printBill(billDetails.toString()) } else { Toast.makeText(this, "蓝牙权限被拒绝,无法打印", Toast.LENGTH_SHORT).show() } } }
自定义账单格式(ESC/POS命令使用)
是的,自定义账单格式必须使用ESC/POS命令,这是热敏POS打印机通用的标准指令集,用于控制字体大小、对齐、加粗、换行、切纸等操作。
1. 常用ESC/POS命令常量定义
// ESC/POS 基础命令 private const val ESC = 27.toChar() private const val GS = 29.toChar() // 对齐方式 const val ALIGN_LEFT = "${ESC}a0" const val ALIGN_CENTER = "${ESC}a1" const val ALIGN_RIGHT = "${ESC}a2" // 字体大小 const val FONT_NORMAL = "${ESC}!" + 0.toChar() const val FONT_DOUBLE_WIDTH = "${ESC}!" + 16.toChar() const val FONT_DOUBLE_HEIGHT = "${ESC}!" + 32.toChar() const val FONT_BIG = "${ESC}!" + 48.toChar() // 宽高加倍 // 加粗控制 const val BOLD_ON = "${ESC}E1" const val BOLD_OFF = "${ESC}E0" // 切纸命令 const val CUT_PAPER = "${GS}V1" // 换行 const val NEW_LINE = "\n"
2. 构建格式化账单示例
修改generateBill函数,加入格式控制:
fun generateBill() { val billDetails = StringBuilder() // 标题:居中、加粗、大字体 billDetails.append(ALIGN_CENTER) billDetails.append(BOLD_ON) billDetails.append(FONT_BIG) billDetails.append("SARAS 账单详情") billDetails.append(NEW_LINE) billDetails.append(NEW_LINE) // 重置为默认格式 billDetails.append(BOLD_OFF) billDetails.append(FONT_NORMAL) billDetails.append(ALIGN_LEFT) // 账单内容 billDetails.append("================================") billDetails.append(NEW_LINE) billDetails.append("商品类型:${dataItem.rcdfAssismentitem[0].ItemType}") billDetails.append(NEW_LINE) billDetails.append("单位编码:${dataItem.rcdfAssismentitem[0].UnitCode}") billDetails.append(NEW_LINE) billDetails.append("数量(LTR):${dataItem.rcdfAssismentitem[0].QtyLTR}") billDetails.append(NEW_LINE) billDetails.append("单价:${dataItem.rcdfAssismentitem[0].Rate}") billDetails.append(NEW_LINE) billDetails.append("================================") billDetails.append(NEW_LINE) // 右下角打印时间 billDetails.append(ALIGN_RIGHT) billDetails.append("打印时间:${SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date())}") billDetails.append(NEW_LINE) billDetails.append(NEW_LINE) // 切纸 billDetails.append(CUT_PAPER) printBill(billDetails.toString()) }
3. 注意事项
- 不同品牌打印机对ESC/POS命令可能有细微差异,建议查阅对应打印机的官方文档确认支持的指令。
- 格式命令需在打印文本前发送,使用后记得重置为默认格式,避免影响后续打印内容。
- 测试时可先打印简单的格式指令,确认打印机响应后再构建完整账单。
内容的提问来源于stack exchange,提问作者Sati Anshul
相关产品推荐
相关产品推荐

