Android 蓝牙实战 - 蓝牙打印

680 阅读7分钟

前言

蓝牙技术在Android开发中已成为了必须会的技能

蓝牙在Android开发中的重要性体现在它为用户提供了便利的无线连接方式,并为开发者提供了丰富的应用场景,包括数据传输、外部设备控制等。使用蓝牙技术,可以为用户提供更便捷的体验,并为开发者提供创新应用的可能性。因此蓝牙技术在Android开发中不可或缺

下面介绍 蓝牙打印在实战中的实现方式
权限 -> 搜索 -> 连接 -> 生成指令 -> 下发给打印机
蓝牙打印使用的是apache旗下的Velocity模板、将此模板转为ESC/CPCL/TSPL指令,并下发到打印机

蓝牙搜索

前置条件

权限

权限: android12 以后api31,蓝牙权限有更改
12以后仍需要位置权限,因为部分ble会搜索不到,以防万一都申请

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
@RequiresApi(Build.VERSION_CODES.S)
@Permission(
    permissions = [Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_ADVERTISE, Manifest.permission.BLUETOOTH_CONNECT,
        Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION],
    rejects = ["app需请求位置权限提供蓝牙搜索服务"])
}else{
@Permission(
    permissions = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION],
    rejects = ["app需请求位置权限提供蓝牙搜索服务"])
}

权限申请以后,需要(校验)打开蓝牙、打开GPS(api>23)

打开蓝牙
fun openBT() {
    if (!BtUtil.isOpen()) {
        if (!BtUtil.checkBluetoothConnectPermission()){
            return
        }
        val enableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(enableIntent, ENABLE_BT)
    }
}
打开GPS
fun openGPS() {
    if (Build.VERSION.SDK_INT >= 23 && !PowerUtils.isGPSAvailable(this)) {
        //            // 如果GPS不可用   打开gps
        val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
        startActivityForResult(intent, ENABLE_GPS)
    }
}

蓝牙搜索

普通蓝牙:

1.将已连接的设备添到列表里

//已配对蓝牙
val pairedDevices: Set<BluetoothDevice> =
    BluetoothAdapter.getDefaultAdapter().bondedDevices
    
//过滤出打印机(需要的设备)  
for (device in pairedDevices) {
    if (device?.bluetoothClass?.deviceClass == BtUtil.PRINT_TYPE
        || device?.bluetoothClass?.deviceClass == BtUtil.PRINT_TYPE_UNKNOW
    ) {
        bluetoothChoseDialog?.addData(device)
    }
}

2.搜索新设备蓝牙添加到列表里

/**
 * 搜索蓝牙设备
 */
fun searchDevices(adapter: BluetoothAdapter?) {
    if (!checkBluetoothConnectPermission()|| !checkBluetoothScanPermission()) {
        return
    }
    // 寻找蓝牙设备,android会将查找到的设备以广播形式发出去
    adapter?.startDiscovery()
}

3.接收广播,将搜索到的设备过滤出所需的设备
注册广播

/**
 * register bluetooth receiver
 *
 * @param receiver bluetooth broadcast receiver
 * @param activity activity
 */
@JvmStatic
fun registerBluetoothReceiver(receiver: BroadcastReceiver?, activity: Activity?) {
    if (null == receiver || null == activity) {
        return
    }
    val intentFilter = IntentFilter()
    //start discovery
    intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED)
    //finish discovery
    intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
    //bluetooth tabId change
    intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
    //found device
    intentFilter.addAction(BluetoothDevice.ACTION_FOUND)
    //bond tabId change
    intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
    //pairing device
    intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
    activity.registerReceiver(receiver, intentFilter)
}

搜索到广播后回调

/**
 * 普通蓝牙扫描回调 - 广播返回
 */
private var mBtReceiver: BroadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (!BtUtil.checkBluetoothConnectPermission()){
            return
        }
        when (intent.action) {
            BluetoothDevice.ACTION_FOUND -> {
            //搜索到设备
                var device =
                    intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
                if (afterFindDevice(device)) return
            }
            BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
                //识别结束
                bluetoothChoseDialog?.endSearch()
            }

            BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
            //设备连接或断开
                var device =
                    intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
                if (device?.bondState == BluetoothDevice.BOND_BONDED) {
                    connectBTDevice(device, 0)
                } else if (device?.bondState == BluetoothDevice.BOND_NONE) {
                    ToastUtils.showToast("配对失败,请重试")
                    bluetoothChoseDialog?.endSearch()
                }
            }
        }
    }
}
ble蓝牙
mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
...
mBluetoothLeScanner.startScan(leScanCallback);
leScanCallback为搜索出回调的ble设备

蓝牙连接

普通蓝牙 连接蓝牙
public BluetoothSocket connectDevice(BluetoothDevice device, Context context) {
    //权限校验
    if (!BtUtil.checkBluetoothConnectPermission()) {
        ToastUtils.showToast("请打开蓝牙权限");
       return null;
    }
    try {
        if (socket != null) {
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        Log.e(TAG_LOG,"removeMessages connectDevice" + CLOSE_SOCKET);
        mHandler.removeMessages(CLOSE_SOCKET);
        socket = device.createInsecureRfcommSocketToServiceRecord(MY_UUID);
        socket.connect();
    } catch (IOException ex) {
        ex.printStackTrace();
        try {
            socket =(BluetoothSocket) device.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(device,1);
            socket.connect();
        } catch (Exception e2) {
            Log.e("", "Couldn't establish Bluetooth connection!");
            if (socket != null) {
                try {
                    socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            //关闭蓝牙
           // BluetoothAdapter.getDefaultAdapter().disable();
        }
        return null;
    }
    PrintQueue.getQueue(context).tryConnect(socket);
    return socket;
}
ble蓝牙 连接蓝牙
//蓝牙连接
public void getConnectDevice(BluetoothDevice bluetoothDevice, KmBlebluetoothListener callback) {
    LogUtil.d("开始连接---");
    //停止扫描
    stopBluetoothDevicesDiscovery();
    connectBack = callback;
    selectBleTooth = bluetoothDevice;
    if (bluetoothDevice == null) {
        LogUtil.d("this device is not exist");
        return;
    }
    if (isConnecting(bluetoothDevice.getAddress()) && connectBack!=null) {
        connectBack.connectSuccess();
        return;
    }
    
    BluetoothGattCallback gattCallback = new BluetoothGattCallback() {

        @Override
        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
            super.onMtuChanged(gatt, mtu, status);
            LogUtil.d("-->-- state" + status + " maxPackageSize " + maxPackageSize);
            maxPackageSize = mtu - 3;
            gatt.discoverServices();
        }

        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            LogUtil.d("连接状态回传----" + status + "state" + newState);

            connectState = newState;
            if (status == 0) { //表示gatt服务成功
                if (newState == BluetoothProfile.STATE_CONNECTED) {  // 连接成功状态
                    //连接成功之后的操作
                    alreadyConnection = true;
                    LogUtil.d("连接成功");
                    connectBluetoothDevices = bluetoothDevice.getAddress();
                    mGatt = gatt;
                    mGatt.requestMtu(185);
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // 断开连接状态
                    //断开连接之后的操作
                    LogUtil.d("断开连接");
                    closeBluetoothAdapter();
                    cleanConnectDevice();
                }
            } else {
                cleanConnectDevice();
                gatt.disconnect();
                gatt.close();
                if (status == 133) { //经典错误:133
                    LogUtil.d("133 —— 需要重连 isRetry = " + isRetry);
                    //提示:此处我们可以根据全局的一个flag,去重试连接一次,以增大连接成功率
                    if (!isRetry) {
                        LogUtil.d("133 —— 重连一次");
                        isRetry = true;
                        getConnectDevice(selectBleTooth, connectBack);
                    } else {
                        LogUtil.d("133 —— 重连失败");
                        connectBack.connectError();
                    }
                } else {
                    connectBack.connectError();
                }
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            LogUtil.d("onServicesDiscovered: **********************************");
            //获取服务列表  服务列表中有当前设备的uuid
            List<BluetoothGattService> servicesList = gatt.getServices();
            LogUtil.d("services:" + servicesList.size());

            for (BluetoothGattService service : servicesList) {
                List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
                for (BluetoothGattCharacteristic character : characteristics) {
                    LogUtil.d("-->--" + service.getUuid() + "  characterId:" + character.getUuid() + " " + character.getProperties());
                    if (character.getProperties() == 12) {
                        if (alreadyConnection) {
                            connectBack.connectSuccess();
                            alreadyConnection = false;
                        }
                        serviceId = service.getUuid();
                        characterId = character.getUuid();
                        LogUtil.d("serviceId:" + service.getUuid());
                        LogUtil.d("characterId:" + character.getUuid());
                        // mGatt.requestMtu(400);
                        return;
                    }
                }
            }
        }
    };

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        LogUtil.d("开始连接");
        mGatt = bluetoothDevice.connectGatt(mContext.get(),
                false, gattCallback, BluetoothDevice.TRANSPORT_LE);  //

    } else {
        mGatt = bluetoothDevice.connectGatt(mContext.get(),
                false, gattCallback);

    }
}

蓝牙连接成功后,得将设备信息存到本地,以后在使用可以直接用deviceId直接连接。

蓝牙打印

连接打印机
  1. 获取设备列表(连接后存到本地的或者数据库里的)
  2. 校验设备状态(是否打开蓝牙、是否有绑定设备、权限、连接蓝牙...)
  3. 请求获取打印模板(请求接口或者本地取)
  4. 处理数据(打印数据、vm模板数据、打印份数、自定义key)
public class PrinterBean implements Serializable {
    /**
     * 打印多少张小票,默认打印一张
     */
    public int printCount = 1;
    /**
     * 小票模板信息(velocity模板)
       (用InputStreamReader将xxx.vm文件读取出模板信息)
     */
    public String templateInfo;
    /**
     * 小票模板velocity中一级Bean的key
       (固定值)
     */
    public String templateBeanKey;

    /**
     * 小票模板velocity中一级Bean
       (实际数据)
     */
    public Object templateBean;
}
生成打印指令ESC(/CPCL/TSPL)
  1. (将数据)生成ESC命令
    1.使用的是apache旗下的Velocity模板,将数据转为String

String printInfo = render.render(printerBean.templateInfo, contentMap);

/**
 * 生成内容
 */
@Override
public String render(String templateInfoString, Map<String, Object> values) {
    VelocityContext context = new VelocityContext(values);
    StringWriter writer = new StringWriter();
    try{
        VelocityEngine ve = new VelocityEngine();
        ve.setProperty(VelocityEngine.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.NullLogChute");
        ve.init();
        // 转换输出
        ve.evaluate(context, writer, "", templateInfoString); // 关键方法
    }catch (Exception e){
        e.printStackTrace();
    }
    return writer.toString();
}

2.将string转为esc命令

//生成ESC命令
boolean needCut = true;
EscCommand esc = getEscCommand(printInfo, printerBean.printCount, needCut);

Vector<Byte> command = esc.getCommand();
int length = esc.getCommand().size();
byte[] commend = new byte[length];
for (int i = 0; i < length; i++) {
    commend[i] = command.get(i);
}

参考此类
github.com/dongyonghui…

 /**
     * 根据模板内容,将某一行的文本信息解析成打印机命令数组
     *
     * @param rowInfo 根据模板生成的文件的某一行信息
     * @param esc     解析成打印机命令数组会保存到这里
     */
    private static void parseRow(String rowInfo, EscCommand esc) {
        if (TextUtils.isEmpty(rowInfo)) {
            return;
        }
        //如果有标签
        if (Pattern.matches(".*<[a-zA-Z]+>.*", rowInfo)) {
            try {
                XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
                XmlPullParser parser = factory.newPullParser();
                parser.setInput(new StringReader(rowInfo));
                int eventType = parser.getEventType();
                while (eventType != XmlPullParser.END_DOCUMENT) {
                    String nodeName = parser.getName();
                    switch (eventType) {
                        // 开始解析某个结点
                        case XmlPullParser.START_TAG: {
                            Log.d("Printer", "标签开始{ nodeName = " + nodeName);
                            if (FormatTagConstants.TAG_C.equalsIgnoreCase(nodeName)) {//居中
                                setCenterStyle(esc);
                            } else if (FormatTagConstants.TAG_B.equalsIgnoreCase(nodeName)) {//放大2倍
                                setBiggerStyle(esc);
                            } else if (FormatTagConstants.TAG_L.equalsIgnoreCase(nodeName)) {//高放大2倍
                                setHeightMoreStyle(esc);
                            } else if (FormatTagConstants.TAG_W.equalsIgnoreCase(nodeName)) {//宽放大2倍
                                setWidthMoreStyle(esc);
                            } else if (FormatTagConstants.TAG_CB.equalsIgnoreCase(nodeName)) {//居中放大
                                setBiggerCenterStyle(esc);
                            } else if (FormatTagConstants.TAG_RIGHT.equalsIgnoreCase(nodeName)) {//右对齐
                                setTextRightStyle(esc);
                            } else if (FormatTagConstants.TAG_QR.equalsIgnoreCase(nodeName)) {//二维码
                                tempNodeName = nodeName;
                            } else if (FormatTagConstants.TAG_CODE.equalsIgnoreCase(nodeName)) {//条形码
                                tempNodeName = nodeName;
                            } else if (FormatTagConstants.TAG_BR.equalsIgnoreCase(nodeName)) {//换行
                                esc.addFeedLine();
                                setTextLeftStyle(esc);
                            } else if (FormatTagConstants.TAG_CUT.equalsIgnoreCase(nodeName)) {//切纸
                                esc.addCutPaperAndFeed(BluetoothPrintManager.CUT_PAPER_AND_FEED);
                            } else if (FormatTagConstants.TAG_PLUGIN.equalsIgnoreCase(nodeName)) {//钱箱
                                esc.addOpenCashDawer(TscCommand.FOOT.F5, (byte) 1, (byte) 0);
                            } else if (FormatTagConstants.TAG_BOLD.equalsIgnoreCase(nodeName)) {//加粗
                                setTextBoldStyle(esc);
                            } else if (FormatTagConstants.TAG_ROW.equalsIgnoreCase(nodeName)) {//行
                                columInfoList = new ArrayList<>();
                            } else if (FormatTagConstants.TAG_COL.equalsIgnoreCase(nodeName)) {//列
                                columInfo = new ColumInfo();
                                for (int i = 0; i < parser.getAttributeCount(); i++) {
                                    if (FormatTagConstants.TAG_COL_WEIGHT.equalsIgnoreCase(parser.getAttributeName(i))) {
                                        columInfo.weight = parser.getAttributeValue(i);
                                    } else if (FormatTagConstants.TAG_COL_PADDING.equalsIgnoreCase(parser.getAttributeName(i))) {
                                        columInfo.padding = parser.getAttributeValue(i);
                                    } else if (FormatTagConstants.TAG_COL_GRAVITY.equalsIgnoreCase(parser.getAttributeName(i))) {
                                        columInfo.gravity = parser.getAttributeValue(i);
                                    } else if (FormatTagConstants.TAG_COL_EOL.equalsIgnoreCase(parser.getAttributeName(i))) {
                                        columInfo.eol = parser.getAttributeValue(i);
                                    }
                                }
                            }
                            break;
                        }
                        // 完成解析某个结点
                        case XmlPullParser.END_TAG: {
                            Log.d("Printer", "标签结束} nodeName = " + nodeName);
                            if (FormatTagConstants.TAG_CB.equalsIgnoreCase(nodeName)//居中放大
                                    || FormatTagConstants.TAG_B.equalsIgnoreCase(nodeName)//放大2倍
                                    || FormatTagConstants.TAG_L.equalsIgnoreCase(nodeName)//高放大2倍
                                    || FormatTagConstants.TAG_W.equalsIgnoreCase(nodeName)) {//宽放大2倍
                                resetTextSize(esc);
                            } else if (FormatTagConstants.TAG_BOLD.equalsIgnoreCase(nodeName)) {//加粗
                                resetTextBoldStyle(esc);
                            } else if (FormatTagConstants.TAG_QR.equalsIgnoreCase(nodeName)) {//二维码
                                tempNodeName = null;
                            } else if (FormatTagConstants.TAG_CODE.equalsIgnoreCase(nodeName)) {//条形码
                                tempNodeName = null;
                            } else if (FormatTagConstants.TAG_ROW.equalsIgnoreCase(nodeName)) {//行
                                RowTool rowTool = new RowTool();
                                StringBuilder stringBuilder = new StringBuilder();
                                for (ColumInfo info : columInfoList) {
                                    stringBuilder.append(rowTool.format(info.eol, info.text,
                                            String.valueOf(info.weight)
                                            , info.padding, info.gravity));
                                }
                                esc.addText(stringBuilder.toString());
                                columInfoList = null;
                            } else if (FormatTagConstants.TAG_COL.equalsIgnoreCase(nodeName)) {//列
                                columInfo = null;
                            }
                            break;
                        }
                        case XmlPullParser.TEXT:
                            Log.d("Printer", "文本内容--------" + parser.getText());
                            if (FormatTagConstants.TAG_QR.equalsIgnoreCase(tempNodeName)) {
                                addQRCode(parser.getText(), esc);
                            } else if (FormatTagConstants.TAG_CODE.equalsIgnoreCase(tempNodeName)) {
                                addBarCode(parser.getText(), esc);
                            } else if (null != columInfo && null != columInfoList) {
                                columInfo.text = parser.getText();
                                columInfoList.add(columInfo);
                            } else {
                                esc.addText(parser.getText());
                            }
                            break;
                        default:
                            break;
                    }
                    eventType = parser.next();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            esc.addText(rowInfo);
        }
    }
下发给打印机
  1. 开启打印服务,将esc指令发送给打印机
    参考此项目:github.com/dongyonghui…
//开启打印服务
Intent intent = new Intent();
intent.setAction(MyPrinterService.ACTION_PRINT_DEFAULT);
Bundle bundle = new Bundle();
bundle.putByteArray("data", commend);
bundle.putInt("totalLineHeight", getLineHeight(printInfo, printerBean.printCount, needCut));
intent.putExtras(bundle);
MyPrinterService.enqueueWork(GlobalApplication.getContext(), intent);
Log.e(TAG_LOG,"removeMessages printCode" + CLOSE_SOCKET);
mHandler.removeMessages(CLOSE_SOCKET);
public class MyPrinterService extends JobIntentService {
    public static final String ACTION_PRINT_DEFAULT = "action_print_default";//打印
    private int mNum;
    static final int JOB_ID = 1001;

    public static void enqueueWork(Context context, Intent work) {
        enqueueWork(context, MyPrinterService.class, JOB_ID, work);
    }

    @Override
    protected void onHandleWork(@NonNull Intent intent) {

        String countNum = intent.getStringExtra("countNum");
        if (!TextUtils.isEmpty(countNum)) {
            mNum = Integer.parseInt(countNum);
        }
        byte[] printData = intent.getByteArrayExtra("data");
        //单次打印的总行高
        int totalLineHeight = intent.getIntExtra("totalLineHeight", 10);
        printCode(printData, totalLineHeight);
    }

    private void printCode(byte[] byteArrayExtra, int totalLineHeight) {
        if (null == byteArrayExtra || byteArrayExtra.length <= 0) {
            BluetoothPrintManager.getInstance().sendNotify(OnPrinterNotifyListener.NotifyMessage.PRINT_FINISH);
            return;
        }
        PrintQueue.getQueue(getApplicationContext()).add(byteArrayExtra, totalLineHeight);
    }
}
public synchronized void print() {
    try {
        if (null == mQueue || mQueue.size() <= 0) {
            return;
        }
        if (null == mAdapter) {
            mAdapter = BluetoothAdapter.getDefaultAdapter();
        }
        if (null == mBtService) {
            mBtService = new BtService(mContext);
        }
        //如果设备未连接,则检查是否有已绑定过的设备,如果有则进行连接尝试
        if (mBtService.getState() != BtService.STATE_CONNECTED) {
            if (!TextUtils.isEmpty(BluetoothPrintManager.getDefaultBluetoothDeviceAddress(mContext))) {
                BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothPrintManager.getDefaultBluetoothDeviceAddress(mContext));
                mBtService.connect(device);
                BluetoothPrintManager.getInstance().sendNotify(OnPrinterNotifyListener.NotifyMessage.WAITING_CONNECT_DEVICE);
                return;
            }
        }
        //通知开始打印
        BluetoothPrintManager.getInstance().sendNotify(OnPrinterNotifyListener.NotifyMessage.PRINT_START);
        while (mQueue.size() > 0) {
            mBtService.write(mQueue.get(0));
            mQueue.remove(0);
            //20行等待1秒,最少等待1秒
            waitTime = Math.max((long) ((mQueueLineHeight.get(0) / 20.0) * 1000), 1000)/1000;
            while (waitTime > 0) {
                Thread.sleep(1000);
                waitTime--;
            }
            mQueueLineHeight.remove(0);
        }

        BluetoothPrintManager.getInstance().sendNotify(OnPrinterNotifyListener.NotifyMessage.PRINT_FINISH);
    } catch (Exception e) {
        BluetoothPrintManager.getInstance().sendNotify(OnPrinterNotifyListener.NotifyMessage.PRINT_FAILED_PRINT_ERROR);
        e.printStackTrace();
    }
}

参考文献

蓝牙连接:developer.android.google.cn/guide/topic…
打印机下发指令:github.com/dongyonghui…