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

如何将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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 21:42:10