最近、レシート印刷の作業をしており、プロジェクトの要件としてiOSとAndroidの両方で実装する必要がありました。最初は全く分からず、インターネットで多くの資料を探し、たくさんの落とし穴にはまり、多くの記事を読みましたが、結果的にはうまくいきました。 Bluetoothプリンターは一般的に、レシート印刷とラベル印刷の2種類の印刷モードに分けられます。
会社が購入した粗悪なプリンターには開発ドキュメントすらなく、多くの落とし穴にはまる羽目になりました。開発担当者に購入時に相談してくれればよかったのに。
現在、WeChatミニプログラムでBluetoothプリンターに接続する wx.createBLEConnection は、iOSデバイスでは問題なく動作しますが、一部のAndroidスマートフォンでは異常が発生します(接続時にシステムペアリングボックスがポップアップ表示され、キャンセルをタップしても、ペアリングコードを入力して確定をタップしても、すぐに接続が切断されます。入力もキャンセルもしない場合、30秒以内にBluetoothプリンターから自動的に切断されます)。
現在採用している方法は、AndroidとiOSそれぞれにBluetooth印刷コマンドのセットを作成することです。 IOS
// ====================蓝牙操作================== //初始化蓝牙模块
openBluetoothAdapter() {
if (app.sysinfo.provider == 1) {
// 开启蓝牙
app.onBluetooth()
setTimeout(() => {
this.android_search()
}, 2000)
return false;
}
this.closeBluetoothAdapter()
uni.openBluetoothAdapter({
success: (res) => {
console.log("初始化蓝牙模块: " + JSON.stringify(res));
this.startBluetoothDevicesDiscovery()
},
fail: (res) => {
if (res.errCode === 10001) {
uni.onBluetoothAdapterStateChange((res) => {
console.log('监听蓝牙适配器状态变化事件', res)
if (res.available == false) {
app.global_printing = {}
this.connected = false
this.chs = []
this.canWrite = false
}
if (res.available) {
this.startBluetoothDevicesDiscovery()
}
})
}
if (res.errCode) {
app.alert('初始化蓝牙失败,错误码:' + res.errCode)
return false;
}
app.alert(res.errMsg)
}
})
},
//获取本机蓝牙适配器状态
getBluetoothAdapterState() {
uni.getBluetoothAdapterState({
success: (res) => {
console.log('获取本机蓝牙适配器状态。', JSON.stringify(res))
if (res.discovering) {
this.onBluetoothDeviceFound()
} else if (res.available) {
this.startBluetoothDevicesDiscovery()
}
},
fail: (res) => {
console.log('error:获取本机蓝牙适配器状态失败', JSON.stringify(res))
setTimeout(() => {
this.getBluetoothAdapterState()
}, 500)
}
})
},
//开始搜寻附近的蓝牙外围设备
startBluetoothDevicesDiscovery() {
console.log(this.discoveryStarted);
if (this.discoveryStarted) {
return
}
console.log('开始搜索蓝牙设备');
this.discoveryStarted = true
this.onBluetoothDeviceFound()
setTimeout(() => {
uni.startBluetoothDevicesDiscovery({
allowDuplicatesKey: true,
success: (res) => {
console.log('startBluetoothDevicesDiscovery success', JSON.stringify(
res))
},
fail: (res) => {
if (res.errCode == '10001') {
app.alert('当前蓝牙适配器不可用')
} else {
app.alert('搜索蓝牙失败,状态码:' + res.errCode)
}
}
})
}, 500)
},
// 停止搜索
stopBluetoothDevicesDiscovery() {
uni.stopBluetoothDevicesDiscovery()
this.discoveryStarted = false
},
//寻找到新设备的事件的回调函数
onBluetoothDeviceFound() {
console.log('寻找到新设备的事件的回调函数');
uni.onBluetoothDeviceFound((res) => {
console.log(res);
res.devices.forEach(device => {
if (!device.name && !device.localName) {
return
}
const foundDevices = this.devices
const idx = this.inArray(foundDevices, 'deviceId', device.deviceId)
if (idx === -1) {
this.devices.push(device)
} else {
this.devices[idx] = device
}
})
})
},
//连接低功耗蓝牙设备
createBLEConnection(e) {
uni.showLoading({
title: '设备连接中',
mask: true
});
const ds = e.currentTarget.dataset
const deviceId = ds.deviceId
const name = ds.name
if (app.sysinfo.provider == 1) {
if (ds.pair !== true) {
this.android_search(deviceId)
} else {
console.log('已配对')
}
var device = null,
BAdapter = null,
BluetoothAdapter = null,
uuid = null,
main = null,
bluetoothSocket = null;
var mac_address = deviceId
var main = plus.android.runtimeMainActivity();
BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter");
var UUID = plus.android.importClass("java.util.UUID");
uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
BAdapter = BluetoothAdapter.getDefaultAdapter();
device = BAdapter.getRemoteDevice(mac_address);
plus.android.importClass(device);
bluetoothSocket = device.createInsecureRfcommSocketToServiceRecord(uuid);
plus.android.importClass(bluetoothSocket);
if (!bluetoothSocket.isConnected()) {
console.log('检测到设备未连接,尝试连接....');
bluetoothSocket.connect();
}
this.connected = true
this.name = name
this.deviceId = deviceId
this.canWrite = true
app.global_printing = {
name: name,
deviceId: deviceId
}
app.saveData1('global_printing', app.global_printing)
uni.hideLoading();
return false;
}
uni.createBLEConnection({
deviceId,
success: (res) => {
this.connected = true
this.name = name
this.deviceId = deviceId
app.global_printing = {
name: name,
deviceId: deviceId
}
this.onBLEConnectionStateChange()
// 防止获取失败
setTimeout(() => {
this.getBLEDeviceServices(deviceId)
}, 1000)
},
fail: (res) => {
uni.hideLoading();
app.Toast('设备连接失败')
console.log("蓝牙连接失败:", res);
}
})
this.stopBluetoothDevicesDiscovery()
},
//获取蓝牙设备所有服务(service)
getBLEDeviceServices(deviceId) {
uni.getBLEDeviceServices({
deviceId,
success: (res) => {
console.log("获取蓝牙服务成功:" + JSON.stringify(res))
if (res.services.length == 0) {
uni.hideLoading();
app.alert('没有获取到蓝牙服务,无法打印001')
app.global_printing = {}
return false
}
for (let i = 0; i < res.services.length; i++) {
if (res.services[i].isPrimary) {
this.getBLEDeviceCharacteristics(deviceId, res.services[i].uuid)
return
}
}
},
fail: (res) => {
setTimeout(() => {
this.getBLEDeviceServices(deviceId)
}, 500)
console.log("获取蓝牙服务失败:" + JSON.stringify(res))
}
})
},
//获取蓝牙设备某个服务中所有特征值(characteristic)
getBLEDeviceCharacteristics(deviceId, serviceId) {
console.log('获取蓝牙设备某个服务中所有特征值', deviceId, serviceId)
uni.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => {
console.log('获取蓝牙设备某个服务中所有特征值 success', JSON.stringify(res))
uni.hideLoading();
if (res.characteristics.length == 0) {
app.alert('没有获取到蓝牙服务,无法打印002')
app.global_printing = {}
return false
}
for (let i = 0; i < res.characteristics.length; i++) {
let item = res.characteristics[i]
if (item.properties.read) {
uni.readBLECharacteristicValue({
deviceId,
serviceId,
characteristicId: item.uuid,
})
}
if (item.properties.write) {
this.canWrite = true
app.global_printing._deviceId = deviceId
app.global_printing._serviceId = serviceId
app.global_printing._characteristicId = item.uuid
app.saveData1('global_printing', app.global_printing)
//this.writeBLECharacteristicValue()
}
if (item.properties.notify || item.properties.indicate) {
uni.notifyBLECharacteristicValueChange({
deviceId,
serviceId,
characteristicId: item.uuid,
state: true,
})
}
}
},
fail(res) {
console.error('获取特征值失败:', res)
}
})
// 操作之前先监听,保证第一时间获取数据
uni.onBLECharacteristicValueChange((characteristic) => {
console.log(this.data.chs);
const idx = this.inArray(this.data.chs, 'uuid', characteristic.characteristicId)
const data = {}
if (idx === -1) {
this.chs[this.data.chs.length] = {
uuid: characteristic.characteristicId,
value: ab2hex(characteristic.value)
}
} else {
this.chs[idx] = {
uuid: characteristic.characteristicId,
value: ab2hex(characteristic.value)
}
}
})
},
onBLEConnectionStateChange() {
uni.onBLEConnectionStateChange((res) => {
// 该方法回调中可以用于处理连接意外断开等异常情况
console.log(`蓝牙连接状态改变device ${res.deviceId} state has changed, connected: ${res.connected}`)
if (res.connected == false) {
app.global_printing = {}
this.connected = false
this.chs = []
this.canWrite = false
}
})
},
//断开与低功耗蓝牙设备的连接
closeBLEConnection() {
app.global_printing = {}
uni.closeBLEConnection({
deviceId: this.deviceId
})
this.connected = false
this.chs = []
this.canWrite = false
},
//关闭蓝牙模块
closeBluetoothAdapter() {
app.global_printing = {}
uni.closeBluetoothAdapter()
this.discoveryStarted = false
},
//发送数据
sendStr(bufferstr, success, fail) {
var that = this;
uni.writeBLECharacteristicValue({
deviceId: app.global_printing._deviceId,
serviceId: app.global_printing._serviceId,
characteristicId: app.global_printing._characteristicId,
value: bufferstr,
success: function(res) {
success(res);
console.log('发送的数据:' + bufferstr)
// console.log('message发送成功')
},
fail: function(res) {
fail(res)
console.log("数据发送失败:" + JSON.stringify(res))
},
complete: function(res) {
// console.log("发送完成:" + JSON.stringify(res))
}
})
},
//遍历发送数据
printCode(arr) {
var that = this;
if (arr.length > 0) {
this.sendStr(arr[0], function(success) {
arr.shift();
that.printCode(arr);
}, function(error) {
app.alert('打印失败,错误码:' + error.errCode)
app.printing_status = false
console.log(error);
});
return false;
}
setTimeout(function() {
app.printing_status = false
console.log('打印结束');
}, 1000);
},Android
比較的シンプルで便利です。Native.jsを使用してNative Javaインターフェースチャネルを直接呼び出し、plus.androidを介してAndroidネイティブシステムAPIを呼び出します。
ネイティブAndroidドキュメント https://developer.android.google.cn/reference/android/bluetooth/BluetoothAdapter?hl=en
// ======================Android============
// 搜索蓝牙设备
android_search(address = '') {
//搜索、配对
var main = plus.android.runtimeMainActivity();
var IntentFilter = plus.android.importClass('android.content.IntentFilter');
var BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter");
var BluetoothDevice = plus.android.importClass("android.bluetooth.BluetoothDevice");
var BAdapter = BluetoothAdapter.getDefaultAdapter();
console.log("开始搜索设备");
var filter = new IntentFilter();
var bdevice = new BluetoothDevice();
var on = null;
var un = null;
console.log('正在搜索请稍候');
BAdapter.startDiscovery(); //开启搜索
var receiver;
receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
onReceive: (context, intent) => { //实现onReceiver回调函数
plus.android.importClass(intent); //通过intent实例引入intent类,方便以后的‘.’操作
// console.log(intent.getAction()); //获取action
if (intent.getAction() == "android.bluetooth.adapter.action.DISCOVERY_FINISHED") {
main.unregisterReceiver(receiver); //取消监听
console.log("搜索结束")
} else {
var BleDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//判断是否配对
if (BleDevice.getBondState() == bdevice.BOND_NONE) {
console.log("未配对蓝牙设备:" + BleDevice.getName() + ' ' + BleDevice.getAddress());
//参数如果跟取得的mac地址一样就配对
if (address == BleDevice.getAddress()) {
if (BleDevice.createBond()) { //配对命令.createBond()
if (BleDevice.getName() != null) {
console.log("配对成功蓝牙设备:" + BleDevice.getName() + ' ' +
BleDevice.getAddress());
// app.Toast("配对成功蓝牙设备:" + BleDevice.getName())
}
} else {
console.log('配对失败')
}
} else {
if (BleDevice.getName() != on) { //判断防止重复添加
on = BleDevice.getName();
if (BleDevice.getName() != null) {
this.devices.push({
deviceId: BleDevice.getAddress(),
name: BleDevice.getName()
})
console.log("搜索到蓝牙设备:" + BleDevice.getName() + ' ' +
BleDevice.getAddress());
}
}
}
} else {
if (BleDevice.getName() != un) { //判断防止重复添加
un = BleDevice.getName();
if (BleDevice.getName() != null) {
this.devices.push({
deviceId: BleDevice.getAddress(),
name: BleDevice.getName() + ' (已配对)',
pair: true
})
console.log("已配对蓝牙设备:" + BleDevice.getName() + ' ' + BleDevice.getAddress());
}
}
}
}
}
});
filter.addAction(bdevice.ACTION_FOUND);
filter.addAction(BAdapter.ACTION_DISCOVERY_STARTED);
filter.addAction(BAdapter.ACTION_DISCOVERY_FINISHED);
filter.addAction(BAdapter.ACTION_STATE_CHANGED);
main.registerReceiver(receiver, filter); //注册监听
},
// 打印
android_printCode(arr) {
var that = this;
// 打印
var device = null,
BAdapter = null,
BluetoothAdapter = null,
uuid = null,
main = null,
bluetoothSocket = null;
var mac_address = app.global_printing.deviceId
var main = plus.android.runtimeMainActivity();
BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter");
var UUID = plus.android.importClass("java.util.UUID");
uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
BAdapter = BluetoothAdapter.getDefaultAdapter();
try {
device = BAdapter.getRemoteDevice(mac_address);
plus.android.importClass(device);
bluetoothSocket = device.createInsecureRfcommSocketToServiceRecord(uuid);
plus.android.importClass(bluetoothSocket);
} catch (e) {
console.log('asasssds-d=da=da-dsd');
app.printing_status = false
app.alert('打印失败')
return false;
}
if (!bluetoothSocket.isConnected()) {
console.log('检测到设备未连接,尝试连接....');
bluetoothSocket.connect();
}
console.log('设备已连接');
if (bluetoothSocket.isConnected()) {
var outputStream = bluetoothSocket.getOutputStream();
plus.android.importClass(outputStream);
for (var i = 0; i < arr.length; i++) {
outputStream.write(arr[i]);
}
outputStream.flush();
device = null //这里关键
bluetoothSocket.close(); //必须关闭蓝牙连接否则意外断开的话打印错误
}
setTimeout(function() {
app.printing_status = false
console.log('打印结束');
}, 1000);
},印刷コマンド(その他の印刷コマンドについては https://www.jianshu.com/p/dd6ca0054298 を参照)
/**
* 复位打印机
*/
public static final byte[] RESET = {0x1b, 0x40};
/**
* 左对齐
*/
public static final byte[] ALIGN_LEFT = {0x1b, 0x61, 0x00};
/**
* 中间对齐
*/
public static final byte[] ALIGN_CENTER = {0x1b, 0x61, 0x01};
/**
* 右对齐
*/
public static final byte[] ALIGN_RIGHT = {0x1b, 0x61, 0x02};
/**
* 选择加粗模式
*/
public static final byte[] BOLD = {0x1b, 0x45, 0x01};
/**
* 取消加粗模式
*/
public static final byte[] BOLD_CANCEL = {0x1b, 0x45, 0x00};
/**
* 宽高加倍
*/
public static final byte[] DOUBLE_HEIGHT_WIDTH = {0x1d, 0x21, 0x11};
/**
* 宽加倍
*/
public static final byte[] DOUBLE_WIDTH = {0x1d, 0x21, 0x10};
/**
* 高加倍
*/
public static final byte[] DOUBLE_HEIGHT = {0x1d, 0x21, 0x01};
/**
* 字体不放大
*/
public static final byte[] NORMAL = {0x1d, 0x21, 0x00};
/**
* 设置默认行间距
*/
public static final byte[] LINE_SPACING_DEFAULT = {0x1b, 0x32};QRコードの印刷について 上記の記事から、以下のことが分かります。
生成されたQRコードのピクセル点のRGBAを読み取り、画像データをまず4つを1つにまとめて0か1かを判断し(0は印刷、1は印刷しない)、次に8つを1つにまとめます。1バイトは8ビットだからです。最後に、プリンターのビットマップコマンドを使用して、行ごとにスキャンして印刷します。
4-in-1
QRコードは黒か白だと思っていたので、値は255か0のどちらかだと確信していましたが、実際には一部に他の値が含まれることがありますので注意が必要です。4ビットごとに1ピクセル点のRGBAを表し、白黒のRGBは(0,0,0)と(255,255,255)です。そのため、4ビットごとに最初のビットだけを白黒化し、その最初のビットを新しい配列として取り出します。rule > 200 の場合、値は0となり印刷しないことを示し、それ以外の場合は1となり印刷することを示します。
8-in-1
例えば、取り出した8ビットの数字が [0,0,0,0,0,0,0,1] の場合、これを8つを1つにまとめるには、基数変換を行う必要があります。右から左へ2の0乗、2の1乗と順に加算していき、実際には 0 * 2^7 + 0 * 2^6 + 0 * 2^5 + 0 * 2^4 + 0 * 2^3 + 0 * 2^2 + 0 * 2^1 + 1 * 2^0 となります。この数値が最終的に必要なデータの一つです。
データをArrayBufferに変換し、さらに印刷にはコマンドが必須です!参照URLと標準のESC-POSコマンドセットを参照してください。以下のコード中の数字はすべてコマンドです。また、私のプリンターはgb2312形式をサポートしているため、ArrayBufferに変換すると同時に、エンコーディング形式も正しい形式に変換する必要があります。
ただし、一点注意すべきことがあります。iOSとAndroidの違いに注意してください。Androidは一度に20バイトまでしか書き込めません(iOSは不明ですが、おそらく120バイト)。データ data.slice(20, byteLength) を直接切り取り、印刷が成功したら再度コールバックを呼び出し、ループで印刷することをお勧めします。
// 二维码
qr(text,callback) {
let that = this;
const ctx = uni.createCanvasContext('myQrcode');
ctx.clearRect(0, 0, 240, 240);
drawQrcode({
canvasId: 'myQrcode',
text: String(text),
width: 120,
height: 120,
callback(e) {
// setTimeout(() => {
// 获取图片数据
uni.canvasGetImageData({
canvasId: 'myQrcode',
x: 0,
y: 0,
width: 240,
height: 240,
success(res) {
let arr = that.convert4to1(res.data);
let data = that.convert8to1(arr);
const cmds = [].concat([27, 97, 1], [29, 118, 48, 0, 30, 0,
240, 0
],
data, [27, 74, 3], [27, 64]);
const buffer = toArrayBuffer(Buffer.from(cmds, 'gb2312'));
// 二维码
for (let i = 0; i < buffer.byteLength; i = i + 120) {
that.arrPrint.push(buffer.slice(i, i + 120));
}
callback()
}
})
// }, 3000);
}
});
},1、toArrayBuffer はコンポーネントなのでインストールが必要です。https://www.npmjs.com/package/to-array-buffer または const buffer = new Uint8Array(Buffer.from(cmds, 'gb2312')).buffer; のように記述することもできます。
2、自分のデータが正しいか確認してください。描画データに問題があると、黒いブロックが印刷される可能性があります。
3、データは計算が必要です!!!計算が必要です!!計算が必要です!! 例えば、私が描画した画像が160*160の場合、印刷データに結合するコマンド [29, 118, 48, 0, 20, 0, 160, 0] の中の20と160は計算されたものです。上記の記事を参照して理由を確認してください。おおよそ1:8で、描画データと読み取りデータが一致します。
関連関数 (繰り返しテストの結果、印刷用紙の1行あたりの最大バイト数は32バイトです。これは通常のレシートプリンターを指します) 3列または2列を印刷する場合、自分でスペースを計算して埋める必要があります。既成のコマンドはありません。 全体の幅 - 左側のテキストの長さ - 右側のテキストの長さ がスペースの長さになります。
/**
* 打印两列
*
* @param leftText 左侧文字
* @param rightText 右侧文字
* @return
*/
printTwoData(leftText, rightText) {
var sb = ''
var leftTextLength = this.getBytesLength(leftText);
var rightTextLength = this.getBytesLength(rightText);
sb += leftText
// 计算两侧文字中间的空格
var marginBetweenMiddleAndRight = 32 - leftTextLength - rightTextLength;
for (var i = 0; i < marginBetweenMiddleAndRight; i++) {
sb += ' '
}
sb += rightText
return sb.toString();
},
/**
* 打印三列
*
* @param leftText 左侧文字
* @param middleText 中间文字
* @param rightText 右侧文字
* @return
*/
printThreeData(leftText, middleText, rightText) {
var sb = ''
// 左边最多显示 8 个汉字 + 两个点
if (leftText.length > 8) {
leftText = leftText.substring(0, 8) + "..";
}
var leftTextLength = this.getBytesLength(leftText);
var middleTextLength = this.getBytesLength(middleText);
var rightTextLength = this.getBytesLength(rightText);
sb += leftText
// 计算左侧文字和中间文字的空格长度
var marginBetweenLeftAndMiddle = 20 - leftTextLength - middleTextLength / 2;
for (var i = 0; i < marginBetweenLeftAndMiddle; i++) {
sb += ' '
}
sb += middleText
// 计算右侧文字和中间文字的空格长度
var marginBetweenMiddleAndRight = 12 - middleTextLength / 2 - rightTextLength;
for (var i = 0; i < marginBetweenMiddleAndRight; i++) {
sb += ' '
}
sb += rightText
// 打印的时候发现,最右边的文字总是偏右一个字符,所以需要删除一个空格
// sb.delete(sb.length() - 1, sb.length()).append(rightText);
return sb.toString();
},
max(n1, n2) {
return Math.max(n1, n2)
},
len(arr) {
arr = arr || []
return arr.length
},
//4合1
convert4to1(res) {
let arr = [];
for (let i = 0; i < res.length; i++) {
if (i % 4 == 0) {
let rule = 0.29900 * res[i] + 0.58700 * res[i + 1] + 0.11400 * res[i + 2];
if (rule > 200) {
res[i] = 0;
} else {
res[i] = 1;
}
arr.push(res[i]);
}
}
return arr;
},
//8合1
convert8to1(arr) {
let data = [];
for (let k = 0; k < arr.length; k += 8) {
let temp = arr[k] * 128 + arr[k + 1] * 64 + arr[k + 2] * 32 + arr[k + 3] * 16 + arr[k + 4] * 8 +
arr[k + 5] * 4 +
arr[k + 6] * 2 + arr[k + 7] * 1
data.push(temp);
}
return data;
},
inArray(arr, key, val) {
for (let i = 0; i < arr.length; i++) {
if (arr[i][key] === val) {
return i;
}
}
return -1;
},
// ArrayBuffer转16进度字符串示例
ab2hex(buffer) {
var hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function(bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return hexArr.join('');
},
// 计算文字占用长度
getBytesLength(str) {
var num = str.length; //先用num保存一下字符串的长度(可以理解为:先假设每个字符都只占用一个字节)
for (var i = 0; i < str.length; i++) { //遍历字符串
if (str.charCodeAt(i) > 255) { //判断某个字符是否占用两个字节,如果是,num再+1
num++;
}
}
return num; //返回最终的num,既是字符串总的字节长度
}

