Android蓝牙连接打印机实战指南

Android蓝牙打印实战教程
AI助手已提取文章相关产品:

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在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 判断前后台状态,合理调度蓝牙行为。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在Android平台上实现蓝牙连接打印机是移动办公和物联网应用中的常见需求。本文详细讲解如何使用Android蓝牙API(如BluetoothAdapter、BluetoothDevice和BluetoothSocket)完成蓝牙设备的开启、扫描、配对与连接,并通过输出流发送基于ESC/POS等协议的打印指令,实现文本打印、图形绘制、切纸等操作。同时涵盖连接管理、异常处理、用户提示及电源优化策略,提升应用稳定性与用户体验。结合提供的JavaApk源码说明与bluetoothprinter示例项目,开发者可快速掌握蓝牙打印的核心技术并应用于实际开发中。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值