本篇我们将讲解蓝牙打印机和wifi云打印机的连接与数据发送,下一篇讲解ESC/POS命令集
一、蓝牙打印机连接
打印机的蓝牙连接方式是基于传统的蓝牙连接方式,手机作为客户端,打印机作为服务端。
我们先上效果图:
1.蓝牙权限
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- If your app targets Android 9 or lower, you can declare
ACCESS_COARSE_LOCATION instead. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_PERMISSION);
}
BLUETOOTH权限允许用户请求连接,接受连接和传输数据等,BLUETOOTH_ADMIN权限允许应用启动设备发现或操纵蓝牙设置。如果应用的目标版本是Android 9或者更低的版本,ACCESS_COARSE_LOCATION权限允许蓝牙扫描收集用户的位置信息,返回的是一个模糊的位置信息,此信息可能来自用户自己的设备,以及在商店和交通设施等位置使用蓝牙信标。Android 10开始,要使用蓝牙扫描位置信息需要申请ACCESS_FINE_LOCATION权限,返回的是精确的位置信息,除此之外,还需要开启GPS功能才行,不然不能搜索和连接其他蓝牙设备。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
if (!lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
showToast("请您先开启gps,否则蓝牙不可用");
return;
}
}
2.初始化配置
初始化设备本身的蓝牙适配器BluetoothAdapter,有两种方式:
//方式一:
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//方式二:
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
整个系统只有一个蓝牙适配器,全局只有一个实例,如果返回null,则代表设备不支持蓝牙,如果设备支持蓝牙,再接着检查蓝牙是否打开:
if (bluetoothAdapter == null || !getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
showToast("当前设备不支持蓝牙");
finish();
return;
} else {
if (!bluetoothAdapter.isEnabled()) {
//请求开启蓝牙
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BLE);
} else {
setPairingDevice();
handler.postDelayed(new Runnable() {
@Override
public void run() {
scanDevice();
}
}, 1000);
}
}
成功打开蓝牙后就会回调到onActivityResult()中。除了这种主动的打开蓝牙,还可以监听BluetoothAdapter.ACTION_STATE_CHANGED广播,每当蓝牙状态发生变化时,此广播包含的值BluetoothAdapter.EXTRA_STATE,它包含新的蓝牙状态,可能的值:BluetoothAdapter.STATE_OFF和BluetoothAdapter.STATE_ON。
3.发现设备
设备发现是一个扫描过程,它会搜索局部区域内已开启蓝牙功能的设备,并请求与每台设备相关的某些信息。如果设备已开启可检测行,它会通过共享一些信息(例如设备名称、类及其唯一的MAC地址)来响应发现请求。扫描是一个耗时的过程,我们需要在异步执行,并且监听发现设备的广播。
Set<BluetoothDevice> devices = bluetoothAdapter.getBondedDevices();//获取已配对的设备
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
bluetoothAdapter.startDiscovery();
}
}, 1000);
private BroadcastReceiver discoveryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (TextUtils.isEmpty(action) || bluetoothDevice == null) {
return;
}
switch (action) {
case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
Log.e("TAG", "正在搜索附近的蓝牙设备");
break;
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
Log.e("TAG", "搜索结束");
break;
case BluetoothDevice.ACTION_ACL_CONNECTED:
Log.e("TAG", "与" + bluetoothDevice.getName() + "蓝牙已连接");
break;
case BluetoothDevice.ACTION_ACL_DISCONNECTED:
Log.e("TAG", "与" + bluetoothDevice.getName() + "蓝牙连接已结束");
break;
case BluetoothDevice.ACTION_FOUND:
Log.e("TAG", "发现了新设备");
if (bluetoothDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
//Add
}
break;
case BluetoothAdapter.ACTION_STATE_CHANGED:
int blueState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
switch (blueState) {
case BluetoothAdapter.STATE_OFF:
showToast("蓝牙已关闭");
finish();
break;
case BluetoothAdapter.STATE_ON:
showToast("蓝牙已开启");
Set<BluetoothDevice> devices = bluetoothAdapter.getBondedDevices();//获取已配对的设备
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
bluetoothAdapter.startDiscovery();
}
}, 1000);
break;
}
break;
}
}
};
注意:startDiscovery()只能扫描到那些状态被设为可发现的设备。安卓设备默认不可发现,要改变设备为可发现的状态,需要如下请求:
//无功能状态,查询扫描和页面扫描都无效,该状态下蓝牙模块既不能扫描其他设备,也不可见
//请求开启可见
Intent discoveryIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoveryIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivityForResult(discoveryIntent, REQUEST_DISCOVERABLE_BLE);
注意:startDiscovery()是一个特别耗费资源的操作,所以为了避免资源浪费,需要及时的调用cancelDiscovery()来释放资源。比如在进行设备连接之前,一定要先调用cancelDiscovery()。
4.连接设备
蓝牙设备的连接和网络连接的模型十分相似,都是Client-Server模式,都通过一个socket来进行数据传输。作为一个Android设备,存在以下三种情况:
1.只作为Client端发起连接
2.只作为Server端等待别人发起建立连接的请求
3.同时作为Client和Server
因为我们这篇文章主要为介绍连接热敏打印机的做铺垫,所以这里我们只讲Android设备作为Client建立连接的情况。因为打印机也不可能主动跟Android设备建立连接,所以打印机必然是作为Server端被连接。
4.1 作为Client连接
- 首先需要获取一个BluetoothDevice对象。获取方式如前文介绍的,通过调用startDiscovery()并监听广播获得,也可以通过查询已配对的设备获得。
- 通过BluetoothDevice.createInsecureRfcommSocketToServiceRecord(UUID)得到BluetoothSocket对象。
- 通过BluetoothSocket.connect()建立连接
- 异常处理以及连接关闭
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
BluetoothSocket tmp = null;
mmDevice = device;
try {
// 通过 BluetoothDevice 获得 BluetoothSocket 对象
tmp = device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
} catch (IOException e) { }
mmSocket = tmp;
}
@Override
public void run() {
// 建立连接前记得取消设备发现
mBluetoothAdapter.cancelDiscovery();
try {
// 耗时操作,所以必须在主线程之外进行
mmSocket.connect();
} catch (IOException connectException) {
//处理连接建立失败的异常
try {
mmSocket.close();
} catch (IOException closeException) { }
return;
}
doSomething(mmSocket);
}
//关闭一个正在进行的连接
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
Client发起连接时传入的UUID必须要和Server端设置的一样,都在就会报错。
如是像我们连接热敏打印机的这种情况,因为一些常见的蓝牙服务协议已有约定的UUID,比如我们连接热敏打印机是基于SPP串口通信协议,其对应的UUID是"00001101-0000-1000-8000-00805F9B34FB"。
5.数据传输
经过前面4步的操作,两个蓝牙设备已连接,准备就绪,现在就是利用Socket获得InputStream输入流和OutputStream输出流来进行数据得收发。
由于我们是与热敏打印机连接,所以我们只需要给打印机发送我们需要打印得内容。
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
//通过 socket 得到 InputStream 和 OutputStream
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024]; // buffer store for the stream
int bytes; // bytes returned from read()
//不断的从 InputStream 取数据
while (true) {
try {
bytes = mmInStream.read(buffer);
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer).sendToTarget();
} catch (IOException e) {
break;
}
}
}
//向 Server 写入数据
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) { }
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
二、wifi云打印机连接
wifi打印机的连接方式跟蓝牙连接方式一样,都是Client-Server模式,通过一个socket来进行数据传输。由于手里还没有传统的wifi打印机,所以无法验证。但是公司有一台wifi云打印机,通过云服务器连接,调用WebAPI来连接发送数据实现打印。
我们先看一下效果图:
2.1 添加设备
先关注佳博科技的微信公众号,登录佳博云平台http://cloud.poscom.cn/,注册云平台账号,获取API集成所需的商户编号和API密钥。下面是网页端的云平台界面:
我们也可以直接在云平台的终端管理里面去添加终端设备,这里我们就调用API的方式在我们自己的APP中向云服务添加一台我们的打印终端。详细接口文档可以参考:http://cloud.poscom.cn/index.php?catid=18
@POST
@FormUrlEncoded
Observable<CommonResponse> addDevice(@Url String url,
@Field("reqTime") String reqTime,
@Field("securityCode") String securityCode,
@Field("memberCode") String memberCode,
@Field("deviceID") String deviceID,
@Field("devName") String devName);
public Observable<CommonResponse> addDevice(String deviceID, String devName) {
Retrofit retrofit = RetrofitUtils.getGsonRetrofit();
String url = "http://api.poscom.cn/apisc/adddev";
String memberCode = "商户编号";
String reqTime = String.valueOf(System.currentTimeMillis());
String apiKey = "API key";
String securityCode = Md5Utils.md5(memberCode + reqTime + apiKey + deviceID);
return retrofit.create(IWifi.class).addDevice(url, reqTime, securityCode, memberCode, deviceID, devName);
}
注意:securityCode安全校验码,是用API密钥和规定的参数进行MD5运算的结果,注意顺序不能乱。另外,如果云服务器检测到该设备ID已存在,将提示设备已存在,需要先删除再添加。
2.2 删除打印机
@POST
@FormUrlEncoded
Observable<CommonResponse> deleteDevice(@Url String url,
@Field("reqTime") String reqTime,
@Field("securityCode") String securityCode,
@Field("memberCode") String memberCode,
@Field("deviceID") String deviceID);
public Observable<CommonResponse> deleteDevice(String deviceID) {
Retrofit retrofit = RetrofitUtils.getGsonRetrofit();
String url = "http://api.poscom.cn/apisc/deldev";
String memberCode = "商户编号";
String reqTime = String.valueOf(System.currentTimeMillis());
String apiKey = "API key";
String securityCode = Md5Utils.md5(memberCode + reqTime + apiKey + deviceID);
return retrofit.create(IWifi.class).deleteDevice(url, reqTime, securityCode, memberCode, deviceID);
}
注意:securityCode安全校验码的MD5运算顺序。
2.3 查询打印机列表
@POST
@FormUrlEncoded
Observable<ListDeviceResponse> getListDevices(@Url String url,
@Field("reqTime") String reqTime,
@Field("memberCode") String memberCode,
@Field("securityCode") String securityCode);
public Observable<ListDeviceResponse> getListDevices() {
Retrofit retrofit = RetrofitUtils.getGsonRetrofit();
String url = "http://api.poscom.cn/apisc/listDevice";
String memberCode = "商户编号";
String reqTime = String.valueOf(System.currentTimeMillis());
String apiKey = "API key";
String securityCode = Md5Utils.md5(memberCode + reqTime + apiKey);
return retrofit.create(IWifi.class).getListDevices(url, reqTime, memberCode, securityCode);
}
注意:securityCode安全校验码的MD5运算顺序。
2.4 发送数据到打印机
@POST
@FormUrlEncoded
Observable<CommonResponse> sendMsg(@Url String url,
@Field("reqTime") String reqTime,
@Field("securityCode") String securityCode,
@Field("memberCode") String memberCode,
@Field("deviceID") String deviceID,
@Field("mode") String mode,//model 2-3,2自由格式打印,推荐,3十六进制命字符串打印
@Field("msgDetail") String msgDetail);
public Observable<CommonResponse> sendMsg(String deviceID) {
Retrofit retrofit = RetrofitUtils.getGsonRetrofit();
String url = "http://api.poscom.cn/apisc/sendMsg";
String memberCode = "商户编号";
String reqTime = String.valueOf(System.currentTimeMillis());
String apiKey = "API key";
String securityCode = Md5Utils.md5(memberCode + deviceID + reqTime + apiKey);
String mode = "2";
String msgDetail = "<gpLogo/><gpWord Align=1 Bold=1 Wsize=2 Hsize=2 Reverse=0 Underline=0>发货单</gpWord>\n" +
"<gpBarCode Align=1 Type=7 Width=2 Height=80 Position=0>201811080001</gpBarCode>\n" +
"<gpWord Align=0 Bold=0 Wsize=0 Hsize=0 Reverse=0 Underline=0>订单编号:201811080001</gpWord>\n" +
"<gpWord Align=0 Bold=0 Wsize=0 Hsize=0 Reverse=0 Underline=0>买家姓名:张三</gpWord>\n" +
"<gpWord Align=0 Bold=0 Wsize=0 Hsize=0 Reverse=0 Underline=0>买家手机:18666666666</gpWord>\n" +
"<gpWord Align=0 Bold=1 Wsize=0 Hsize=1 Reverse=0 Underline=0>买家留言:发顺丰,尽快发货,谢谢</gpWord>\n" +
"<gpWord Align=0 Bold=1 Wsize=0 Hsize=1 Reverse=0 Underline=0>卖家备注:发顺丰,优先处理</gpWord>\n" +
"<gpWord Align=0 Bold=1 Wsize=0 Hsize=1 Reverse=0 Underline=0>买就送信息:送U盘</gpWord>\n" +
"<gpWord Align=0 Bold=0 Wsize=0 Hsize=0 Reverse=0 Underline=0>--------------------------------</gpWord>\n" +
"<gpTR4 Type=0><td>宝贝名称</td><td>单价</td><td>数量</td><td>价格</td></gpTR4>\n" +
"<gpTR4 Type=0><td>佳博GP-CH421D云打印机</td><td>1180</td><td>1</td><td>1180</td></gpTR4>\n" +
"<gpTR4 Type=0><td>佳博GP-5890XIII云打印机</td><td>480</td><td>1</td><td>480</td></gpTR4>\n" +
"<gpTR4 Type=0><td>佳博G3-350V云打印机</td><td>980</td><td>1</td><td>980</td></gpTR4>\n" +
"<gpTR4 Type=0><td>100x150热敏标签纸300</td><td>36</td><td>10</td><td>360</td></gpTR4>\n" +
"<gpTR4 Type=0><td>58毫米热敏卷纸100米</td><td>48</td><td>5</td><td>240</td></gpTR4>\n" +
"<gpTR4 Type=0><td>80毫米热敏卷纸100米</td><td>42</td><td>10</td><td>420</td></gpTR4>\n" +
"<gpWord Align=0 Bold=0 Wsize=0 Hsize=0 Reverse=0 Underline=0>-------------------------------- </gpWord>\n" +
"<gpWord Align=2 Bold=0 Wsize=0 Hsize=0 Reverse=0 Underline=0>合计:3660元</gpWord>\n" +
"<gpWord Align=2 Bold=0 Wsize=0 Hsize=0 Reverse=0 Underline=0>优惠:-198元</gpWord>\n" +
"<gpWord Align=2 Bold=0 Wsize=0 Hsize=0 Reverse=0 Underline=0>邮费: 30元</gpWord>\n" +
"<gpWord Align=2 Bold=1 Wsize=1 Hsize=1 Reverse=0 Underline=0>实收:3492元 </gpWord>\n" +
"<gpCut/>\n" +
"<gpWord Align=1 Bold=1 Wsize=1 Hsize=1 Reverse=0 Underline=0>扫码关注佳博</gpWord>\n" +
"<gpQRCode Align=1 Size=9 Error=M>http://weixin.qq.com/r/kHV3b67EXPMjreoM9yCC</gpQRCode>\n" +
"<gpCut/>";
return retrofit.create(IWifi.class).sendMsg(url, reqTime, securityCode, memberCode, deviceID, mode, msgDetail);
}
佳博票据云打印格式详情可以参考http://cloud.poscom.cn/index.php?id=152。
注意:securityCode安全校验码的MD5运算顺序不能乱。mode打印信息的类型,mode为2是自由格式打印,如上文格式,mode为3是十六进制命令集或十六进制字符串打印。
最后附上打印出来的效果图: