简介:在Android平台上实现蓝牙连接打印机是移动办公和物联网应用中的常见需求。本文详细讲解如何使用Android蓝牙API(如BluetoothAdapter、BluetoothDevice和BluetoothSocket)完成蓝牙设备的开启、扫描、配对与连接,并通过输出流发送基于ESC/POS等协议的打印指令,实现文本打印、图形绘制、切纸等操作。同时涵盖连接管理、异常处理、用户提示及电源优化策略,提升应用稳定性与用户体验。结合提供的JavaApk源码说明与bluetoothprinter示例项目,开发者可快速掌握蓝牙打印的核心技术并应用于实际开发中。
1. Android蓝牙连接打印机的技术背景与核心架构
在移动应用开发中,无线打印功能已成为提升用户体验的重要环节,尤其在零售、物流、医疗等场景中,Android设备通过蓝牙连接便携式打印机实现小票、标签的即时输出,已形成广泛的应用生态。本章从技术背景出发,剖析Android蓝牙打印的整体架构设计,涵盖核心模块与系统层级。
graph TD
A[Android应用层] --> B[蓝牙API框架]
B --> C[BluetoothAdapter]
C --> D[BluetoothDevice]
D --> E[BluetoothSocket]
E --> F[RFCOMM协议]
F --> G[蓝牙打印机硬件]
重点解析经典蓝牙(Bluetooth Classic)为何在打印场景中仍占主导地位——其基于RFCOMM的串行数据流特性更契合热敏打印机的指令传输需求,相比BLE更适合稳定、持续的小数据量通信。同时,梳理自Android 2.0以来蓝牙API的演进,明确 BLUETOOTH 、 BLUETOOTH_ADMIN 及Android 12新增的 NEARBY_DEVICES 权限使用规范,为后续实践提供理论支撑。
2. 蓝牙适配器初始化与设备发现机制
在 Android 蓝牙打印系统中,连接的第一步并非直接发起通信,而是必须完成对本地蓝牙硬件的识别、激活与环境准备。这一过程构成了整个蓝牙功能启用的基础阶段,其核心任务包括获取蓝牙适配器实例、判断设备支持能力、确保蓝牙服务处于可用状态,并在此基础上启动周边设备扫描流程。本章将深入剖析蓝牙初始化的完整生命周期,从系统级 API 的调用逻辑到用户交互设计,再到扫描行为的兼容性优化,构建一套稳健、可扩展且面向生产环境的设备发现机制。
2.1 获取BluetoothAdapter实例与环境检测
蓝牙适配器( BluetoothAdapter )是 Android 系统中所有蓝牙操作的入口点,它代表了设备上的物理蓝牙模块。只有成功获取该对象并确认其可用性后,才能进行后续的扫描、配对和连接等操作。因此,如何正确地初始化 BluetoothAdapter 并进行全面的环境检测,是开发稳定蓝牙应用的前提条件。
2.1.1 通过BluetoothManager获取适配器引用
自 Android 4.3(API Level 18)起,Google 引入了 BluetoothManager 系统服务作为推荐方式来获取 BluetoothAdapter 实例。相比早期通过 BluetoothAdapter.getDefaultAdapter() 直接静态获取的方式,使用 BluetoothManager 更加符合现代 Android 架构的设计理念——即依赖系统服务管理器进行资源访问。
// 获取 BluetoothManager 系统服务
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager == null) {
Log.e("Bluetooth", "BluetoothManager not available");
return false;
}
// 通过 BluetoothManager 获取默认适配器
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
if (bluetoothAdapter == null) {
Log.e("Bluetooth", "Device does not support Bluetooth");
return false;
}
代码逻辑逐行解读:
- 第1行:通过
getSystemService(Context.BLUETOOTH_SERVICE)获取BluetoothManager实例。这是一个标准的 Context 服务绑定操作。 - 第2–4行:判断
bluetoothManager是否为空。某些低端或特殊定制 ROM 设备可能不提供此服务,需做空值防护。 - 第6–7行:调用
getAdapter()方法获取默认蓝牙适配器。如果返回null,说明当前设备不具备蓝牙硬件支持。 - 第8–10行:若
bluetoothAdapter为null,则记录错误日志并返回失败状态。
⚠️ 参数说明:
-Context.BLUETOOTH_SERVICE:系统定义的服务名常量,用于请求蓝牙管理器。
-BluetoothManager.getAdapter():返回当前设备主蓝牙控制器的引用,仅当设备具备蓝牙功能时非空。
该方法的优势在于封装层次更高,便于未来系统升级时统一处理权限、状态变更等事件。此外,在多适配器场景(如双模蓝牙芯片)下, BluetoothManager 提供了更灵活的设备枚举接口。
2.1.2 判断设备是否支持蓝牙功能
并非所有 Android 设备都内置蓝牙模块,尤其是一些工业平板或低成本 IoT 终端。因此,在尝试任何蓝牙操作前,必须首先验证设备是否具备蓝牙能力。
一种常见的做法是在应用启动时执行如下检查:
private boolean isBluetoothSupported() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
return adapter != null;
}
然而,这种方式存在局限性:即使 getDefaultAdapter() 返回非空,也不能保证蓝牙功能可用。例如,设备可能因固件问题导致蓝牙驱动加载失败,或者用户在设置中永久禁用了蓝牙模块。
为此,应结合硬件特征声明(Hardware Feature)进行更精准的判断:
<!-- 在 AndroidManifest.xml 中声明蓝牙使用 -->
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
此声明表示应用运行依赖于蓝牙硬件。若设备不支持该功能,Google Play 商店将自动过滤安装。对于动态检测,可通过 PackageManager 查询:
PackageManager pm = getPackageManager();
boolean hasBluetooth = pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
| 检测方式 | 准确性 | 使用场景 |
|---|---|---|
BluetoothAdapter.getDefaultAdapter() != null | 中等 | 运行时快速判断 |
hasSystemFeature(FEATURE_BLUETOOTH) | 高 | 安装前或初始化阶段 |
| 反射调用底层 HAL 接口 | 高但复杂 | 特殊定制系统调试 |
上述表格展示了三种不同层级的检测策略。建议采用组合式判断:先通过 PackageManager 做预筛,再通过 BluetoothManager 获取适配器实例以确认运行时可用性。
2.1.3 检查蓝牙硬件状态与可用性
获取到 BluetoothAdapter 后,下一步是确认其当前状态是否已启用。Android 将蓝牙状态分为多种模式,主要通过 getState() 方法读取:
int state = bluetoothAdapter.getState();
switch (state) {
case BluetoothAdapter.STATE_ON:
Log.d("Bluetooth", "Bluetooth is ON");
break;
case BluetoothAdapter.STATE_OFF:
Log.d("Bluetooth", "Bluetooth is OFF");
break;
case BluetoothAdapter.STATE_TURNING_ON:
Log.d("Bluetooth", "Bluetooth is turning ON");
break;
case BluetoothAdapter.STATE_TURNING_OFF:
Log.d("Bluetooth", "Bluetooth is turning OFF");
break;
default:
Log.d("Bluetooth", "Unknown state: " + state);
}
状态码含义解析:
-
STATE_ON:蓝牙完全开启,可执行扫描与连接。 -
STATE_OFF:蓝牙关闭,无法进行任何操作。 -
TURNING_ON/OFF:过渡状态,通常持续几百毫秒至数秒。
为了提升用户体验,应在 UI 层实时反映蓝牙状态。例如,使用 BroadcastReceiver 监听状态变化:
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
handleBluetoothStateChange(state);
}
}, filter);
stateDiagram-v2
[*] --> Off
Off --> TurningOn : 用户请求开启
TurningOn --> On : 初始化完成
On --> TurningOff : 用户关闭
TurningOff --> Off : 关闭成功
On --> [*]
该状态图清晰表达了蓝牙模块的典型生命周期流转。开发者应基于此模型设计状态同步逻辑,避免在 TURNING_ON 阶段误触发扫描导致异常。
2.2 蓝牙功能启用与用户交互引导
尽管技术上可以检测蓝牙状态,但最终控制权属于用户。若蓝牙未开启,应用不能擅自强制启用,而应通过标准化的 Intent 请求授权,实现合规且友好的交互体验。
2.2.1 使用Intent请求开启蓝牙
Android 提供了专用的 Intent 动作 ACTION_REQUEST_ENABLE 来引导用户开启蓝牙:
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
该 Intent 会弹出系统对话框,询问用户是否允许开启蓝牙。这是唯一被官方认可的安全方式,避免了后台静默启用带来的隐私风险。
🔐 安全机制说明:
此操作需要BLUETOOTH_ADMIN权限(Android 11 及以下),但从 Android 12 开始,此类请求由系统统一管控,应用无需显式声明即可发起。
2.2.2 监听蓝牙开启结果的回调处理
由于 startActivityForResult 已在 Android 11 中废弃,推荐使用 ActivityResultLauncher 实现现代化回调:
ActivityResultLauncher<Intent> bluetoothEnabler =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Log.d("Bluetooth", "User enabled Bluetooth");
startDeviceDiscovery(); // 继续执行扫描
} else {
Log.w("Bluetooth", "User denied Bluetooth enabling");
showEnablePromptAgain(); // 提示重试
}
});
// 触发请求
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
bluetoothEnabler.launch(intent);
关键参数说明:
-
RESULT_OK:用户同意开启蓝牙。 -
RESULT_CANCELED:用户拒绝或按返回键取消。
值得注意的是,即使用户点击“确定”,蓝牙也可能因硬件故障未能真正开启。因此,在 RESULT_OK 回调中仍需再次调用 bluetoothAdapter.isEnabled() 做二次验证。
2.2.3 异常情况下的降级提示与重试逻辑
在实际部署中,部分用户可能多次拒绝开启蓝牙。此时应设计合理的降级策略:
private int enableAttemptCount = 0;
private void requestBluetoothEnable() {
if (bluetoothAdapter.isEnabled()) {
startDiscovery();
return;
}
if (enableAttemptCount < MAX_RETRY_ATTEMPTS) {
bluetoothEnabler.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE));
enableAttemptCount++;
} else {
showPersistentNotification(
"请前往设置手动开启蓝牙",
Settings.ACTION_BLUETOOTH_SETTINGS
);
}
}
此逻辑防止无限弹窗打扰用户,同时提供替代路径(跳转设置页面)。此外,可结合 SharedPreferences 记录用户选择倾向,实现个性化提醒频率控制。
graph TD
A[蓝牙已开启?] -- 是 --> B[开始扫描]
A -- 否 --> C{尝试次数 < 最大值?}
C -- 是 --> D[弹出启用请求]
C -- 否 --> E[显示持久通知+跳转设置]
D --> F[等待用户响应]
F --> G{用户同意?}
G -- 是 --> H[验证是否真开启]
G -- 否 --> I[计数+1, 等待下次触发]
该流程图展示了完整的启用引导路径,体现了容错与用户体验平衡的设计思想。
2.3 周边设备扫描流程设计
一旦蓝牙启用,便可启动设备发现流程。Android 使用经典蓝牙的 inquiry 扫描机制查找附近可见设备,这一过程涉及广播接收、数据解析与筛选匹配等多个环节。
2.3.1 启动startDiscovery()与registerReceiver注册广播接收器
扫描通过调用 startDiscovery() 发起:
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
registerReceiver(broadcastReceiver, filter);
bluetoothAdapter.startDiscovery();
对应的广播接收器定义如下:
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
String name = device.getName();
String address = device.getAddress();
short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
Log.d("Discovery", "Found: " + name + " [" + address + "] RSSI=" + rssi);
}
}
};
参数详解:
-
EXTRA_DEVICE:携带发现的BluetoothDevice对象。 -
EXTRA_RSSI:信号强度(Received Signal Strength Indicator),单位 dBm,典型范围 -100 ~ 0。
2.3.2 解析ACTION_FOUND广播中的设备信息
每次发现新设备都会触发 ACTION_FOUND 广播。从中提取的信息可用于构建设备列表:
| 字段 | 来源 | 用途 |
|---|---|---|
| 名称(Name) | device.getName() | 显示给用户 |
| MAC 地址 | device.getAddress() | 唯一标识,用于连接 |
| RSSI | EXTRA_RSSI | 判断距离远近 |
| 设备类别 | device.getBluetoothClass() | 区分打印机、耳机等 |
注意:某些设备可能未设置名称,需回退显示 MAC 地址;另外,MAC 地址格式为 XX:XX:XX:XX:XX:XX ,可用于正则匹配特定厂商(如 00:11:22 对应某品牌打印机)。
2.3.3 设备名称过滤与MAC地址匹配策略
为提高搜索效率,可在接收端实施过滤:
String targetNamePrefix = "Printer";
String deviceName = device.getName();
if (deviceName != null && deviceName.startsWith(targetNamePrefix)) {
discoveredDevices.add(device);
notifyAdapterDataSetChanged();
}
也可基于已知 MAC 白名单加速定位:
Set<String> knownPrinters = new HashSet<>(Arrays.asList(
"00:1A:7D:DA:71:13",
"A4:C1:38:2F:4E:5A"
));
if (knownPrinters.contains(device.getAddress())) {
preferredDevices.offer(device); // 加入优先队列
}
此类策略显著减少无关设备干扰,适用于固定部署场景。
sequenceDiagram
participant App
participant Adapter
participant RemoteDevice
App->>Adapter: startDiscovery()
loop 每个扫描周期
Adapter->>RemoteDevice: Inquiry Request
RemoteDevice-->>Adapter: Response with Name/RSSI
Adapter->>App: Broadcast ACTION_FOUND
App->>App: 解析并过滤设备
end
该序列图揭示了扫描过程中三方协作机制,强调了异步通信的本质。
2.4 扫描优化与兼容性处理
随着 Android 版本演进,蓝牙扫描行为发生显著变化,特别是在权限模型与后台限制方面,亟需针对性优化。
2.4.1 扫描周期控制与超时机制设置
原生 startDiscovery() 默认持续约 12 秒。为避免长时间占用资源,可设置定时中断:
Handler scanTimeoutHandler = new Handler(Looper.getMainLooper());
Runnable timeoutTask = () -> {
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
Log.d("Discovery", "Scan stopped due to timeout");
}
};
scanTimeoutHandler.postDelayed(timeoutTask, SCAN_DURATION_MILLIS); // e.g., 10_000ms
合理设置扫描时长(8–15秒)可在覆盖率与性能间取得平衡。
2.4.2 针对不同Android版本的扫描行为差异应对
| Android 版本 | 扫描要求 | 注意事项 |
|---|---|---|
| ≤ Android 9 | 需要 ACCESS_COARSE_LOCATION | 否则无法收到广播 |
| Android 10+ | 改为 ACCESS_FINE_LOCATION 或 NEARBY_DEVICES | 后者需 targetSdk ≥ 31 |
| Android 12+ | 强制要求 BLUETOOTH_CONNECT 和 NEARBY_DEVICES | 运行时申请 |
示例权限请求代码:
String[] requiredPermissions =
Build.VERSION.SDK_INT >= 31 ?
new String[]{Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.NEARBY_DEVICES} :
new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
requestPermissions(requiredPermissions, REQUEST_CODE_PERMISSIONS);
2.4.3 权限变更监听与动态权限请求补全
即便初次授权,用户仍可能后续撤销权限。可通过 PermissionChecker 定期校验:
@TargetApi(31)
private boolean hasNearbyDevicesPermission() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.NEARBY_DEVICES)
== PackageManager.PERMISSION_GRANTED;
}
// 在扫描前调用
if (!hasNearbyDevicesPermission()) {
requestPermissions(...);
}
此外,注册 BroadcastReceiver 监听 ACTION_PERMISSION_GRANTED_RESULTS 可实现即时反馈闭环。
flowchart LR
A[开始扫描] --> B{权限是否满足?}
B -- 是 --> C[启动 discovery]
B -- 否 --> D[请求缺失权限]
D --> E[等待用户授权]
E --> F{授权成功?}
F -- 是 --> C
F -- 否 --> G[降级提示]
此流程确保权限链完整,是现代 Android 蓝牙开发不可或缺的一环。
3. 蓝牙连接建立与安全通信通道构建
在完成设备扫描并获取目标打印机的基本信息后,下一步的核心任务是建立稳定、可靠的蓝牙连接,并构建可用于数据传输的安全通信通道。这一过程不仅涉及Android系统底层的Socket通信机制,还需综合考虑安全性、兼容性以及用户体验等多重因素。本章将围绕BluetoothDevice对象的操作、RFCOMM通道的创建方式、异步连接线程的设计模式以及配对流程的干预策略展开深入剖析,帮助开发者掌握从发现设备到建立可信赖通信链路的完整技术路径。
3.1 BluetoothDevice对象的获取与属性分析
当用户选择一个候选打印机时,应用程序需基于此前扫描阶段获得的 BluetoothDevice 实例进行后续操作。该对象封装了远程蓝牙设备的关键属性和行为接口,是发起连接请求的基础载体。理解其内部结构和可用方法对于实现精准控制至关重要。
3.1.1 从扫描结果中提取目标打印机设备
在第二章所述的设备扫描过程中,通过注册 BroadcastReceiver 监听 ACTION_FOUND 广播事件,可以持续接收周边蓝牙设备的信息更新。每次广播携带的Intent包含一个 BluetoothDevice 对象,可通过 getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) 提取。为避免重复添加或误识别,建议结合设备名称(如“POS-80”、“Printer_BT”)和MAC地址进行过滤:
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null && device.getName() != null) {
// 过滤出可能为热敏打印机的设备
if (device.getName().contains("POS") ||
device.getName().toLowerCase().contains("printer")) {
discoveredDevices.add(device);
deviceAdapter.notifyDataSetChanged();
}
}
}
}
};
逻辑逐行解读:
- 第4~5行:定义广播接收器,专门处理蓝牙设备被发现的事件。
- 第7行:判断当前接收到的是否为
ACTION_FOUND广播。 - 第9行:从Intent中提取完整的
BluetoothDevice对象引用。 - 第10行:增加空值检查,防止NPE异常。
- 第12~15行:根据设备名称关键字筛选潜在打印机设备,提升用户选择效率。
参数说明 :
BluetoothDevice.EXTRA_DEVICE: 标准字段,用于获取广播中附带的设备对象。device.getName():返回远程设备的蓝牙名称,部分厂商允许自定义;若未设置则可能为空或默认值(如“HC-06”)。- 名称匹配策略应具有容错性,支持模糊查找以适应不同品牌命名习惯。
此外,在实际项目中可引入白名单机制,预先配置已知打印机型号的名称前缀或服务UUID,进一步提高识别准确率。
3.1.2 获取设备名称、MAC地址及服务UUID列表
一旦选定目标设备,必须验证其关键属性以确保连接可行性。其中最重要的三个属性如下表所示:
| 属性 | 方法调用 | 说明 |
|---|---|---|
| 设备名称 | device.getName() | 可读性标识符,常用于UI展示 |
| MAC地址 | device.getAddress() | 全球唯一物理地址,格式为 XX:XX:XX:XX:XX:XX |
| 支持的服务UUID | device.getUuids() (需API 15+) | 表示设备对外暴露的功能服务集合 |
特别地,服务UUID决定了该设备是否支持RFCOMM串行通信协议——这是绝大多数便携式热敏打印机所依赖的数据传输方式。例如,标准SPP(Serial Port Profile)对应的UUID通常为 00001101-0000-1000-8000-00805F9B34FB 。可通过以下代码查询设备支持的服务:
ParcelUuid[] uuids = device.getUuids();
if (uuids != null) {
for (ParcelUuid uuid : uuids) {
Log.d("BT_DEBUG", "Supported UUID: " + uuid.toString());
if (SPP_UUID.equals(uuid.getUuid())) {
supportsSpp = true;
break;
}
}
}
注意 :
getUuids()方法仅在设备已被“发现”且服务发现已完成的情况下才返回有效值。某些低端Android设备在调用此方法前需显式执行fetchUuidsWithSdp()触发SDP查询。
sequenceDiagram
participant App
participant RemoteDevice
App->>RemoteDevice: startDiscovery()
RemoteDevice-->>App: ACTION_FOUND (BluetoothDevice)
App->>RemoteDevice: fetchUuidsWithSdp()
RemoteDevice-->>App: SDP Response with UUID List
App->>App: cache UUIDs for connection decision
上述流程图展示了完整的UUID获取流程。只有经过SDP(Service Discovery Protocol)交互后,本地系统才能获知远端设备提供的具体服务类型。因此,在决定是否尝试连接某台打印机之前,强烈建议先确认其是否声明了SPP服务UUID。
3.1.3 判断设备是否为可信任设备
Android系统内置了“配对/绑定”机制,用于维护与远程设备之间的信任关系。已配对的设备会被记录在系统的蓝牙数据库中,下次连接时无需再次输入PIN码即可自动认证。可通过以下方式判断设备当前的信任状态:
int bondState = device.getBondState();
switch (bondState) {
case BluetoothDevice.BOND_NONE:
Log.i("BT_BOND", "Device is not paired.");
initiatePairing(device); // 触发手动配对
break;
case BluetoothDevice.BOND_BONDED:
Log.i("BT_BOND", "Device is already trusted.");
proceedWithSecureConnection(device);
break;
case BluetoothDevice.BOND_BONDING:
Log.i("BT_BOND", "Pairing in progress...");
break;
}
扩展分析:
-
BOND_NONE:表示尚未建立信任,此时若直接尝试安全连接(使用createRfcommSocketToServiceRecord),系统会弹出配对对话框要求用户确认。 -
BOND_BONDING:正处于配对过程中,应禁用重复点击防止冲突。 -
BOND_BONDED:已成功绑定,可复用已有密钥进行加密通信。
对于企业级应用而言,推荐在首次连接成功后缓存已配对设备的MAC地址,并在启动时自动尝试重连最近使用的打印机,从而显著提升操作流畅度。同时,也可结合SharedPreferences保存设备别名、打印偏好等上下文信息,形成个性化连接策略。
3.2 BluetoothSocket创建与RFCOMM通道选择
建立了对 BluetoothDevice 的理解之后,接下来的关键步骤是创建一个 BluetoothSocket 实例,作为数据收发的端点。Android提供了多种创建Socket的方式,开发者需根据安全性需求和设备兼容性做出合理选择。
3.2.1 使用createRfcommSocketToServiceRecord建立安全连接
最推荐的做法是调用 BluetoothDevice#createRfcommSocketToServiceRecord(UUID) 方法创建一个安全的RFCOMM套接字。这种方式利用SSP(Secure Simple Pairing)协议确保通信加密,适用于对数据完整性要求较高的场景:
private BluetoothSocket createSecureSocket(BluetoothDevice device, UUID uuid)
throws IOException {
try {
return device.createRfcommSocketToServiceRecord(uuid);
} catch (IOException e) {
Log.e("BT_SOCKET", "Failed to create secure socket", e);
throw e;
}
}
关键点解析:
- 参数
uuid必须与目标打印机公布的服务UUID完全一致,否则连接将失败。 - 此方法不会立即连接,仅创建Socket对象;真正的连接需调用
socket.connect()并在子线程中执行。 - 若设备尚未配对,调用
connect()时系统会自动弹出配对请求框,引导用户完成身份验证。
该方式的优点在于传输层具备加密能力,防止中间人攻击(MITM),适合金融、医疗等敏感领域。然而缺点是对老旧打印机(尤其是基于HC-05/HC-06模块的设备)兼容性较差,部分固件无法响应SDP查询或拒绝加密连接。
3.2.2 替代方案createInsecureRfcommSocket使用场景
针对不支持安全连接的旧款打印机,Android提供了非安全连接方式:
Method method;
try {
method = device.getClass().getMethod("createInsecureRfcommSocket", int.class);
BluetoothSocket insecureSocket = (BluetoothSocket) method.invoke(device, 1);
return insecureSocket;
} catch (Exception e) {
Log.w("BT_SOCKET", "Reflection failed for insecure socket", e);
// fallback to other methods
}
说明 :由于
createInsecureRfcommSocket(int port)是非公开API,需通过反射调用。端口号一般设为1,对应传统的COM1模拟通道。
| 对比维度 | 安全连接 | 非安全连接 |
|---|---|---|
| 是否需要配对 | 是 | 否 |
| 数据是否加密 | 是 | 否 |
| 兼容性 | 中高(依赖设备支持) | 高(广泛支持) |
| 推荐使用场景 | 商业收银、订单打印 | 内部测试、临时调试 |
尽管非安全连接更易成功,但因数据明文传输存在泄露风险,生产环境应谨慎启用。建议仅在检测到设备明确不支持SPP加密模式时降级使用,并通过日志告警提示管理员更换硬件。
3.2.3 UUID生成规则与常见打印机服务标识对照
UUID的选择直接影响连接成败。以下是几种常见的打印机服务UUID及其来源说明:
| 打印机类型 | UUID | 来源说明 |
|---|---|---|
| 标准SPP设备 | 00001101-0000-1000-8000-00805F9B34FB | 蓝牙SIG官方分配 |
| 某些国产蓝牙模块 | 0000110A-0000-1000-8000-00805F9B34FB | 替代SPP变体 |
| 自定义服务 | 厂商私有UUID | 需查阅设备文档 |
实践中,许多开发者采用“试探法”遍历多个常见UUID直至连接成功。以下是一个健壮的UUID探测逻辑:
public static final UUID[] COMMON_UUIDS = {
UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"), // Standard SPP
UUID.fromString("0000110A-0000-1000-8000-00805F9B34FB"), // Alternate SPP
UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66"), // Custom legacy
};
for (UUID candidate : COMMON_UUIDS) {
try {
BluetoothSocket testSocket = device.createRfcommSocketToServiceRecord(candidate);
testSocket.connect(); // 尝试连接
Log.d("BT_UUID", "Connected using UUID: " + candidate);
return testSocket; // 成功即返回
} catch (IOException e) {
try { testSocket.close(); } catch (IOException closeEx) { /* ignore */ }
continue; // 继续尝试下一个
}
}
throw new IOException("No valid UUID found for device");
该策略虽增加连接耗时,但极大提升了跨品牌适配能力,尤其适用于需要对接多型号打印机的企业级APP。
3.3 连接线程封装与异步执行机制
蓝牙连接属于典型的I/O阻塞操作,耗时可能长达数秒甚至十几秒。若在主线程调用 connect() ,极易引发ANR(Application Not Responding)错误。因此必须将其移至后台线程处理。
3.3.1 在子线程中调用socket.connect()避免ANR
推荐使用独立的 ConnectThread 类封装连接逻辑:
private class ConnectThread extends Thread {
private final BluetoothSocket socket;
private final BluetoothDevice device;
public ConnectThread(BluetoothDevice device, UUID uuid) {
this.device = device;
BluetoothSocket tmp = null;
try {
tmp = device.createRfcommSocketToServiceRecord(uuid);
} catch (IOException e) {
Log.e("BT_CONNECT", "Socket creation failed", e);
}
socket = tmp;
}
@Override
public void run() {
// Always cancel discovery before connecting
bluetoothAdapter.cancelDiscovery();
try {
socket.connect(); // Blocking call
connectionSuccess(socket); // Notify success
} catch (IOException connectException) {
try {
socket.close();
} catch (IOException closeException) {
Log.e("BT_CONNECT", "Could not close socket", closeException);
}
connectionFailed(connectException);
}
}
public void cancel() {
try {
socket.close();
} catch (IOException e) {
Log.e("BT_CONNECT", "Error closing socket", e);
}
}
}
逐行分析:
- 第10~14行:构造函数中预创建Socket,避免在
run()中抛出异常。 - 第22行:务必在连接前停止扫描,否则某些芯片会拒绝连接请求。
- 第26行:
connect()为阻塞调用,应在子线程中安全执行。 - 第30行:连接成功后回调通知主线程切换UI状态。
- 第35行:无论何种失败都应关闭Socket资源,防止泄漏。
3.3.2 实现连接超时控制与异常中断处理
原生 BluetoothSocket.connect() 不支持超时参数,导致长时间卡死问题。解决方案包括使用 CountDownLatch 配合线程池实现定时中断:
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(connectRunnable);
try {
future.get(10, TimeUnit.SECONDS); // 设置10秒超时
} catch (TimeoutException e) {
future.cancel(true);
Log.w("BT_TIMEOUT", "Connection timed out after 10s");
} finally {
executor.shutdown();
}
优化建议 :可在Application级别维护一个全局连接管理器,统一调度所有蓝牙I/O任务,避免并发冲突和资源争抢。
3.3.3 连接成功后启动数据读写线程
连接建立后,应立即启动 ConnectedThread 负责后续数据交换:
private class ConnectedThread extends Thread {
private final BluetoothSocket socket;
private final InputStream inputStream;
private final OutputStream outputStream;
public ConnectedThread(BluetoothSocket socket) throws IOException {
this.socket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
inputStream = tmpIn;
outputStream = tmpOut;
}
public void write(byte[] bytes) {
try {
outputStream.write(bytes);
} catch (IOException e) {
Log.e("BT_WRITE", "Write failed", e);
}
}
public void cancel() {
try {
socket.close();
} catch (IOException e) {
Log.e("BT_CLOSE", "Close failed", e);
}
}
}
该线程持有输入输出流句柄,后续所有打印指令均通过 write() 方法发送。为保证线程安全,建议使用 synchronized 修饰写入方法或采用线程安全队列缓冲待发数据。
3.4 安全连接策略与配对流程干预
为提升用户体验并减少人工干预,高级应用往往需要主动参与配对流程,实现自动化绑定。
3.4.1 自动配对请求触发与PIN码预置
虽然Android未开放直接设置PIN码的公共API,但可通过反射调用隐藏方法实现自动配对:
Method createBondMethod = device.getClass().getMethod("createBond", (Class[]) null);
Boolean result = (Boolean) createBondMethod.invoke(device);
// 或设置固定PIN(仅限特定ROM支持)
Method setPin = device.getClass().getMethod("setPin", byte[].class);
setPin.invoke(device, "1234".getBytes());
注意:此类操作受系统权限限制,仅在root设备或定制ROM上稳定运行。
3.4.2 监听配对状态变化广播ACTION_BOND_STATE_CHANGED
注册广播接收器以实时监控配对进度:
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
BluetoothDevice dev = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
int prevState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);
if (state == BluetoothDevice.BOND_BONDED && prevState == BluetoothDevice.BOND_BONDING) {
Log.d("BT_PAIRING", "Successfully paired with " + dev.getName());
attemptConnection(dev); // 自动开始连接
}
}
}, filter);
此机制使得整个配对-连接流程可完全自动化,极大简化终端用户操作。
3.4.3 已配对设备缓存复用以提升连接效率
最后,建议将成功连接过的设备信息持久化存储:
{
"last_used_printer": "00:11:22:AA:BB:CC",
"trusted_devices": [
{ "mac": "00:11:22:AA:BB:CC", "name": "POS-80", "uuid": "00001101..." }
]
}
启动时优先尝试连接最近设备,无需重新扫描,显著缩短业务响应时间。
graph TD
A[Start App] --> B{Has Last Printer?}
B -->|Yes| C[Load from Cache]
C --> D[Try Auto Connect]
D --> E{Success?}
E -->|Yes| F[Ready to Print]
E -->|No| G[Start Scanning]
B -->|No| G
G --> H[User Select Device]
H --> I[Pair & Connect]
I --> J[Cache Result]
J --> F
该流程图体现了现代蓝牙打印应用应有的智能连接范式:记忆优先、自动回连、失败降级至手动选择,兼顾效率与鲁棒性。
4. 打印机指令协议解析与打印内容构建
在Android设备通过蓝牙连接便携式热敏打印机的实际应用中,真正决定打印质量与功能完整性的核心环节并非连接本身,而是对 打印机指令协议的精准解析与内容字节流的正确构建 。即便蓝牙链路已成功建立、数据通道畅通无阻,若发送的数据不符合目标打印机所支持的控制语言规范,则最终输出的结果将可能是乱码、空白纸张或异常切纸等不可预测行为。
当前市场上主流的小型热敏打印机(如佳博、星微、汉印、芯烨等品牌)普遍采用基于 ESC/POS(Escape/Point of Sale)指令集 的通信协议。该协议由EPSON公司于上世纪80年代提出,经过多年演进而成为行业事实标准,具备良好的兼容性与扩展能力。理解并掌握这一协议体系,是实现高质量打印的关键所在。
本章节将深入剖析ESC/POS协议的核心结构,详细讲解如何通过字节级操作生成符合规范的打印数据流,并在此基础上实现文本样式控制、图像打印、二维码嵌入等高级功能。同时,针对多任务并发场景下的稳定性问题,还将介绍打印任务队列的设计模式与线程同步机制,确保在复杂业务环境下仍能保持高效稳定的输出表现。
4.1 ESC/POS指令集基础理论与编码结构
ESC/POS协议本质上是一套以特定字节序列为载体的 二进制控制命令集合 ,其命名源于“ESC”即ASCII码中的转义字符 \x1B (十进制27),通常作为一系列功能指令的起始标志。整个协议体系涵盖了文本格式化、图像打印、条码生成、切纸控制等多个维度的功能模块,每个功能均由一个或多个预定义的字节序列触发。
4.1.1 控制命令字节格式与功能分类
ESC/POS指令按前缀可分为三大类:
| 指令类型 | 前缀字节 | 典型用途 |
|---|---|---|
| ESC 开头指令 | 0x1B | 文本对齐、字体切换、部分控制 |
| GS 开头指令 | 0x1D | 二维码、条形码、切纸、图像打印 |
| FS 开头指令 | 0x1C | 字符集选择、特殊符号映射 |
这些指令遵循统一的格式结构: [前缀][功能码][参数] 。例如:
// 设置居中对齐
byte[] alignCenter = new byte[]{0x1B, 0x61, 0x01};
-
0x1B: ESC 前缀 -
0x61: 功能码 ‘a’,表示设置对齐方式 -
0x01: 参数值,1 表示居中
以下是常用指令的功能分类表:
| 类别 | 指令前缀 | 示例指令 | 说明 |
|---|---|---|---|
| 文本对齐 | ESC a | \x1B\x61n | n=0左对齐,1居中,2右对齐 |
| 字体加粗 | ESC E | \x1B\x45\x01 | 开启加粗; \x00 关闭 |
| 字体放大 | GS ! | \x1D\x21n | 高4位控制水平倍率,低4位控制垂直倍率 |
| 切纸 | GS V | \x1D\x56\x00 或 \x1D\x56\x41 | 完全切纸或部分切纸 |
| 图像打印 | GS * | \x1D\x2A | 后接点阵图像数据 |
| 条形码 | GS k | \x1D\x6B | 支持多种条码类型如Code128、EAN13 |
⚠️ 注意:不同厂商可能对某些指令的支持程度存在差异,尤其是图像压缩算法和二维码版本支持方面需进行实机测试验证。
Mermaid 流程图:ESC/POS指令执行流程
graph TD
A[开始打印] --> B{是否需要设置样式?}
B -- 是 --> C[发送ESC/GS指令]
B -- 否 --> D[直接写入文本]
C --> D
D --> E{是否包含图像?}
E -- 是 --> F[转换为点阵+发送GS*]
E -- 否 --> G{是否需打印二维码?}
F --> G
G -- 是 --> H[生成QR Code + GS q]
G -- 否 --> I[发送切纸指令GS V]
H --> I
I --> J[刷新缓冲区]
J --> K[结束]
此流程图展示了从准备到完成一次完整打印任务的标准路径,体现了指令之间的逻辑依赖关系。
4.1.2 文本对齐、加粗、放大等样式指令详解
在实际开发中,仅输出纯文本远远不够,用户往往要求 富文本排版能力 ,包括加粗、字号变化、居中显示等。这必须借助ESC/POS提供的格式化指令来实现。
示例代码:构建带样式的打印内容
public byte[] buildStyledText() {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try {
// 1. 居中对齐
buffer.write(new byte[]{0x1B, 0x61, 0x01});
// 2. 加粗开启
buffer.write(new byte[]{0x1B, 0x45, 0x01});
// 3. 字体放大 x2 (横向和纵向)
buffer.write(new byte[]{0x1D, 0x21, 0x11}); // 0x11 = 0001 0001 → H:2x, V:2x
// 4. 写入标题文本(UTF-8编码)
String title = "订单确认单\n";
buffer.write(title.getBytes(StandardCharsets.UTF_8));
// 5. 恢复默认样式
buffer.write(new byte[]{0x1B, 0x45, 0x00}); // 关闭加粗
buffer.write(new byte[]{0x1D, 0x21, 0x00}); // 恢复正常大小
buffer.write(new byte[]{0x1B, 0x61, 0x00}); // 左对齐
// 6. 正常文本内容
String content = "商品名称:咖啡杯\n价格:¥39.9\n数量:1\n";
buffer.write(content.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
return buffer.toByteArray();
}
代码逻辑逐行分析:
-
buffer.write(new byte[]{0x1B, 0x61, 0x01})
→ 发送居中对齐指令,适用于标题展示。 -
buffer.write(new byte[]{0x1B, 0x45, 0x01})
→ 启用加粗模式,提升关键信息可读性。 -
buffer.write(new byte[]{0x1D, 0x21, 0x11})
→ 设置打印模式为双倍宽高(0x11表示高低4位均为1,对应2倍放大)。 -
title.getBytes(StandardCharsets.UTF_8)
→ 使用UTF-8编码处理中文字符,避免乱码。 - 最后恢复默认状态,防止影响后续打印内容。
📌 参数说明:
-0x1D 0x21 n中的n是一个字节,其高4位控制水平放大倍数(bit4~bit7),低4位控制垂直放大倍数(bit0~bit3)。例如0x11=00010001→ 水平×2,垂直×2。
- 实际支持的最大放大倍数取决于打印机型号,常见为 ×8。
此外,还需注意 换行符使用 \n (LF)而非 \r\n ,因为大多数热敏打印机只识别LF作为换行信号。
4.1.3 切纸指令(GS V)与时序要求
完成打印后,自动切纸是用户体验的重要组成部分。ESC/POS通过 GS V 指令实现切纸功能,但不同参数代表不同的切纸行为。
// 完全切纸(完全切断)
byte[] fullCut = new byte[]{0x1D, 0x56, 0x00};
// 部分切纸(留一小段连接)
byte[] partialCut = new byte[]{0x1D, 0x56, 0x41}; // 或 0x01
执行时机与缓冲区刷新
切纸指令不应立即在写入数据后发送,而应在确保所有数据已被打印机接收并处理完毕后再执行。否则可能导致 未打印完就切纸 的问题。
推荐做法:
// 在OutputStream写入所有内容后添加延迟或等待
outputStream.write(printData);
outputStream.flush(); // 强制刷新缓冲区
// 等待一段时间让打印机处理(保守策略)
Thread.sleep(500);
// 发送切纸指令
outputStream.write(fullCut);
outputStream.flush();
⚠️ 时序建议:
-flush()调用不能保证数据已物理打印,仅表示已提交至蓝牙栈。
- 建议结合硬件反馈(如有)或固定延时(300~800ms)提高可靠性。
- 若打印机支持状态查询(如DLE EOT),应优先使用状态反馈机制。
不同机型兼容性对比表
| 打印机品牌 | 完全切纸指令 | 部分切纸指令 | 是否需要延时 |
|---|---|---|---|
| 佳博GP-P225 | \x1D\x56\x00 | \x1D\x56\x41 | 是(≥500ms) |
| 星微XW-850 | \x1D\x56\x01 | \x1D\x56\x00 | 是(≥300ms) |
| 汉印TP80 | \x1D\x56\x42 | \x1D\x56\x41 | 否(自带缓冲管理) |
因此,在生产环境中应维护一份 设备指令映射表 ,根据设备MAC地址或名称动态选择合适的切纸命令。
4.2 字节流生成与OutputStream写入实践
当蓝牙连接成功并获得 BluetoothSocket.getOutputStream() 后,真正的“打印”动作即转化为向该输出流持续写入符合ESC/POS规范的字节序列。然而,由于无线传输特性及打印机处理能力限制,若不加以控制,极易出现 缓冲区溢出、丢包、延迟累积 等问题。
4.2.1 构建包含UTF-8中文字符的打印内容
中文打印是多数国内应用场景的基本需求。虽然ESC/POS原生支持ASCII,但现代打印机普遍支持通过选择字符编码页(Code Page)来实现Unicode渲染。
示例:安全输出中文字符串
private void printChineseText(OutputStream outputStream) throws IOException {
// 选择中文字符集(GBK)
outputStream.write(new byte[]{0x1B, 0x74, 0x13}); // ESC t 19 (13h)
String text = "欢迎光临小店!\n感谢您的支持。\n";
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
// 分段写入,每段不超过256字节
int offset = 0;
int chunkSize = 256;
while (offset < bytes.length) {
int length = Math.min(chunkSize, bytes.length - offset);
outputStream.write(bytes, offset, length);
outputStream.flush();
// 每次写入后短暂休眠,缓解接收端压力
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
offset += length;
}
}
代码逻辑解读:
-
0x1B 0x74 0x13:切换到PC852字符集(对应GBK/GB2312),不同设备可能需调整参数。 - 使用
getBytes(UTF_8)编码中文,确保Java层无乱码。 - 分段写入 :避免一次性发送大块数据导致蓝牙缓冲区满。
-
Thread.sleep(50):人为引入写入间隔,降低打印机处理压力,尤其适用于低端芯片主控的便携设备。
🔍 参数说明:
-chunkSize=256是经验值,过大会增加丢包风险,过小则效率低下。
- 可根据设备响应速度动态调节休眠时间(如首次尝试100ms,逐步降至20ms)。
4.2.2 图像转换为点阵数据并封装为ESC *指令序列
图像打印是ESC/POS中最复杂的操作之一,涉及图像缩放、灰度化、二值化、点阵编码等多个步骤。
图像打印指令结构(GS *)
GS * m xL xH yL yH [raster data]
-
m: 图像模式(0=normal, 1=doubled width, etc.) -
xL/xH: 宽度低/高位字节(单位:bytes) -
yL/yH: 高度低/高位字节(单位:dots)
Java实现:图像转点阵并打印
public byte[] generateImageCmd(Bitmap bitmap, int mode) {
// 限定最大宽度(通常为384px)
int width = Math.min(bitmap.getWidth(), 384);
int height = bitmap.getHeight();
// 创建缩放后的灰度图
Bitmap scaled = Bitmap.createScaledBitmap(bitmap, width, height, true);
byte[] pixels = convertToGrayscale(scaled);
// 计算字节宽度(每8像素占1字节)
int byteWidth = (width + 7) / 8;
ByteArrayOutputStream cmd = new ByteArrayOutputStream();
try {
// 写入指令头:GS * m xL xH yL yH
cmd.write(0x1D);
cmd.write(0x2A); // *
cmd.write(mode & 0xFF);
cmd.write(byteWidth & 0xFF);
cmd.write((byteWidth >> 8) & 0xFF);
cmd.write(height & 0xFF);
cmd.write((height >> 8) & 0xFF);
// 写入点阵数据
for (int row = 0; row < height; row++) {
for (int i = 0; i < byteWidth; i++) {
byte b = 0;
for (int bit = 0; bit < 8; bit++) {
int pixelX = i * 8 + bit;
if (pixelX < width) {
int index = row * width + pixelX;
// 黑色点置1
if ((pixels[index] & 0xFF) < 128) b |= (1 << (7 - bit));
}
}
cmd.write(b);
}
}
// 添加换行防止图像错位
cmd.write('\n');
} catch (IOException e) {
e.printStackTrace();
}
return cmd.toByteArray();
}
逻辑分析:
-
convertToGrayscale()应返回[0,255]范围内的灰度值数组。 -
b |= (1 << (7 - bit))实现MSB(最高位)在前的位排列,符合打印机要求。 - 每行数据按字节组织,不足8像素补0。
- 结尾添加
\n防止图像后文本粘连。
4.2.3 分段写入防止缓冲区溢出与延迟累积
蓝牙SPP(Serial Port Profile)基于RFCOMM协议,其MTU一般为 1008字节 左右。若一次性发送超过该长度的数据包,系统会自动分片,但可能导致接收端无法及时处理。
推荐写入策略
public void safeWrite(OutputStream os, byte[] data) throws IOException {
final int MAX_CHUNK = 512;
int pos = 0;
while (pos < data.length) {
int len = Math.min(MAX_CHUNK, data.length - pos);
os.write(data, pos, len);
os.flush();
// 根据数据量动态休眠
if (len >= 256) {
SystemClock.sleep(100); // 大块数据稍作停顿
} else {
SystemClock.sleep(20);
}
pos += len;
}
}
✅ 优势:
- 避免ANR(主线程阻塞)
- 减少因缓冲区满导致的IOException
- 提升低端设备兼容性
4.3 多格式打印功能实现
现代移动应用常需在同一张小票中融合文本、图片、条码等多种元素,这对指令编排提出了更高要求。
4.3.1 纯文本打印流程封装
设计通用接口便于调用:
public interface PrintElement {
byte[] toBytes();
}
public class TextElement implements PrintElement {
private final String text;
private final boolean bold;
private final int align; // 0=left, 1=center, 2=right
private final int fontSize;
public byte[] toBytes() {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
applyStyle(buf);
try {
buf.write(text.getBytes(StandardCharsets.UTF_8));
buf.write('\n');
} catch (IOException e) { /* ignore */ }
return buf.toByteArray();
}
}
可进一步构建 PrintDocument 类聚合多个 PrintElement 并统一输出。
4.3.2 Base64图像解码与灰度化处理
接收Base64图像字符串并打印:
String base64Str = "data:image/png;base64,iVBORw0KGgoAAAANSUh...";
byte[] imageData = Base64.decode(base64Str.split(",")[1], Base64.DEFAULT);
Bitmap bmp = BitmapFactory.decodeByteArray(imageData, 0, imageData.length);
byte[] imgCmd = generateImageCmd(bmp, 0);
safeWrite(outputStream, imgCmd);
灰度化函数参考:
private byte[] convertToGrayscale(Bitmap bitmap) {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
byte[] gray = new byte[w * h];
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
int pixel = bitmap.getPixel(j, i);
int r = (pixel >> 16) & 0xff;
int g = (pixel >> 8) & 0xff;
int b = pixel & 0xff;
int grayValue = (int)(0.299 * r + 0.587 * g + 0.114 * b);
gray[i * w + j] = (byte) grayValue;
}
}
return gray;
}
4.3.3 二维码生成与条形码指令嵌入
使用ZXing生成二维码:
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
BitMatrix matrix = new QRCodeWriter().encode("https://example.com", BarcodeFormat.QR_CODE, 200, 200);
// 转为Bitmap再走图像打印流程
Bitmap qrBmp = toBitmap(matrix, 8, Color.BLACK, Color.WHITE);
byte[] qrCmd = generateImageCmd(qrBmp, 0);
safeWrite(os, qrCmd);
条形码可用 GS k 指令:
String barcode = "692837465123";
byte[] barCmd = new byte[barcode.length() + 4];
barCmd[0] = 0x1D;
barCmd[1] = 0x6B;
barCmd[2] = 0x02; // UPC-A
barCmd[3] = barcode.length();
System.arraycopy(barcode.getBytes(), 0, barCmd, 4, barcode.length());
os.write(barCmd);
4.4 打印任务队列管理与线程同步
4.4.1 使用HandlerThread或ExecutorService管理并发请求
private ExecutorService printerExecutor = Executors.newSingleThreadExecutor();
public void enqueuePrintTask(PrintJob job) {
printerExecutor.execute(() -> {
try {
byte[] data = job.build();
safeWrite(outputStream, data);
notifyPrintSuccess(job.getId());
} catch (Exception e) {
notifyPrintFailed(job.getId(), e);
}
});
}
使用单线程池确保顺序执行,避免指令交错。
4.4.2 队列阻塞与优先级调度机制设计
扩展为带优先级的任务队列:
PriorityBlockingQueue<PrintJob> queue = new PriorityBlockingQueue<>(11, Comparator.comparingInt(PrintJob::getPriority));
// 循环消费
while (!Thread.interrupted()) {
PrintJob job = queue.take();
executeJob(job);
}
高优先级任务(如支付凭证)可插队执行。
4.4.3 打印完成状态反馈与UI更新通知
通过LiveData或回调通知前端:
private final MutableLiveData<Boolean> printingStatus = new MutableLiveData<>();
private void notifyPrintSuccess(String id) {
printingStatus.postValue(true);
EventBus.getDefault().post(new PrintEvent(id, SUCCESS));
}
确保UI及时反映打印状态,提升交互体验。
5. 连接稳定性保障与生产级应用优化
5.1 蓝牙连接状态实时监控
在实际生产环境中,蓝牙链路的不稳定性是影响打印成功率的主要因素之一。设备距离过远、信号干扰、电源管理策略等都可能导致连接中断。因此,建立一套完整的连接状态监控机制至关重要。
5.1.1 通过Socket输入输出流状态判断连通性
最直接的方式是定期尝试从 BluetoothSocket 的 InputStream 中读取数据或向 OutputStream 写入探测指令。若发生 IOException ,则认为连接已断开。
public boolean isConnectionAlive() {
try {
// 发送一个不会影响打印内容的空指令(如换行)
outputStream.write(new byte[]{0x0A});
outputStream.flush();
return true;
} catch (IOException e) {
return false;
}
}
该方法应在独立线程中周期性调用(如每10秒一次),避免阻塞主线程。
5.1.2 注册ACL_DISCONNECTED广播监听意外断开
Android系统提供了底层蓝牙链路状态变更的广播通知,可通过注册 ACTION_ACL_DISCONNECTED 实时感知设备断开:
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED);
registerReceiver(connectionReceiver, filter);
private final BroadcastReceiver connectionReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device.getAddress().equals(connectedPrinterMac)) {
handleDisconnection(device);
}
}
}
};
此方式响应迅速,适用于检测非主动断开场景。
5.1.3 心跳包机制检测链路活性
为提升检测精度,可设计心跳协议:客户端定时发送特定字节序列(如 0x06 ),打印机回传确认码。未收到回应即判定为失联。
| 心跳参数 | 建议值 | 说明 |
|---|---|---|
| 发送间隔 | 15s | 平衡及时性与能耗 |
| 超时阈值 | 5s | 单次请求最大等待时间 |
| 连续失败次数 | 3 | 触发重连前允许的最大失败次数 |
| 指令字节 | 0x05 ~ 0x0F | 避免与ESC/POS控制符冲突 |
sequenceDiagram
participant App
participant Printer
loop Heartbeat Cycle
App->>Printer: SEND(0x06)
Printer-->>App: ACK(0x07)
Note right of App: 若超时未响应,计数+1
end
alt 失败次数≥3
App->>App: 触发断线处理流程
end
5.2 断线重连与容错恢复策略
5.2.1 指数退避算法实现自动重试
为防止频繁无效连接导致资源浪费,采用指数退避策略:
public class RetryManager {
private static final int MAX_RETRY_DELAY = 60_000; // 最大延迟60秒
private int retryCount = 0;
public long getNextDelay() {
long delay = (long) Math.pow(2, retryCount) * 1000; // 2^n 秒
retryCount++;
return Math.min(delay, MAX_RETRY_DELAY);
}
public void reset() {
retryCount = 0;
}
}
结合 Handler 或 ScheduledExecutorService 实现延时重连:
handler.postDelayed(reconnectRunnable, retryManager.getNextDelay());
5.2.2 记录最近连接设备实现快速回连
将最后一次成功连接的打印机 MAC 地址持久化存储,应用启动时优先尝试直连:
SharedPreferences sp = getSharedPreferences("printer_prefs", MODE_PRIVATE);
String lastMac = sp.getString("last_connected_mac", null);
if (lastMac != null) {
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(lastMac);
connectToDevice(device); // 异步连接
}
5.2.3 打印任务暂存与断点续打设计
使用 Room 数据库暂存待打印任务:
@Entity(tableName = "print_tasks")
public class PrintTask {
@PrimaryKey
public String taskId;
public String content;
public int status; // 0: pending, 1: printing, 2: success, 3: failed
public long createdAt;
public int retryCount;
}
当检测到断线时,将当前任务状态置为“failed”,并在重连成功后查询数据库中未完成任务继续执行。
5.3 异常捕获与用户友好提示
5.3.1 分类处理常见异常情况
| 异常类型 | 可能原因 | 处理建议 |
|---|---|---|
SocketTimeoutException | 网络延迟或设备无响应 | 提示“设备未响应,请检查电源” |
IOException | 连接中断或写入失败 | 触发重连流程 |
SecurityException | 权限缺失 | 引导用户授予权限 |
BluetoothNotEnabledException | 蓝牙未开启 | 跳转设置页面 |
5.3.2 分级提示机制
- Toast :轻量提示,如“正在重连第2次”
- Dialog :严重错误需用户干预,如“打印机未配对,请前往设置”
- Notification :后台任务失败时提醒,支持点击重试
5.3.3 错误码体系设计
定义统一错误码便于日志追踪和上报分析:
public interface ErrorCode {
int ERROR_CONNECT_TIMEOUT = 1001;
int ERROR_WRITE_FAILED = 1002;
int ERROR_PRINTER_BUSY = 1003;
int ERROR_LOW_BATTERY = 1004;
int ERROR_PAPER_JAM = 1005;
}
配合 Log.e("BT_Print", "Error: " + errorCode) 输出结构化日志。
5.4 资源释放与能耗优化措施
5.4.1 正确关闭资源
务必在断开连接或应用退出时释放资源:
public void closeConnection() {
try {
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
if (socket != null) socket.close();
} catch (IOException e) {
Log.e("BT_Close", "Failed to close streams", e);
} finally {
socket = null;
inputStream = null;
outputStream = null;
}
}
同时注销广播接收器:
unregisterReceiver(connectionReceiver);
5.4.2 扫描节能控制
限制扫描时间不超过30秒,并及时停止:
new Handler().postDelayed(() -> {
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
}, 30_000);
5.4.3 生命周期管理
在 onPause() 中暂停非必要蓝牙操作,在 onResume() 中恢复:
@Override
protected void onPause() {
super.onPause();
if (!isAppInForeground()) {
stopHeartbeat();
releaseTemporaryResources();
}
}
通过 ProcessLifecycleOwner 判断前后台状态,合理调度蓝牙行为。
简介:在Android平台上实现蓝牙连接打印机是移动办公和物联网应用中的常见需求。本文详细讲解如何使用Android蓝牙API(如BluetoothAdapter、BluetoothDevice和BluetoothSocket)完成蓝牙设备的开启、扫描、配对与连接,并通过输出流发送基于ESC/POS等协议的打印指令,实现文本打印、图形绘制、切纸等操作。同时涵盖连接管理、异常处理、用户提示及电源优化策略,提升应用稳定性与用户体验。结合提供的JavaApk源码说明与bluetoothprinter示例项目,开发者可快速掌握蓝牙打印的核心技术并应用于实际开发中。
Android蓝牙打印实战教程
5884

被折叠的 条评论
为什么被折叠?



