前言
蓝牙技术在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直接连接。
蓝牙打印
连接打印机
- 获取设备列表(连接后存到本地的或者数据库里的)
- 校验设备状态(是否打开蓝牙、是否有绑定设备、权限、连接蓝牙...)
- 请求获取打印模板(请求接口或者本地取)
- 处理数据(打印数据、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)
- (将数据)生成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);
}
/**
* 根据模板内容,将某一行的文本信息解析成打印机命令数组
*
* @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);
}
}
下发给打印机
- 开启打印服务,将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…