熱敏藍牙印表機開發

熱敏藍牙印表機開發

最近在做小票打印這塊,項目需求是IOS和安卓兩種都要實現,開始做的時候也是一臉懵,然後網上找了不少資料,踩了一堆坑,看了好多文章,結果還好成了
藍牙打印機一般分為兩種打印模式,票據打印、標籤打印

公司買的渣渣打印機連開發文檔都沒有,害我走了不少坑,讓我開發買的時候也不諮詢諮詢我
目前微信小程序連接藍牙打印機 wx.createBLEConnection 測試在IOS設備上沒有問題,在部分安卓手機上會出現異常(表現為,連接是會彈出系統配對框,不管點取消還是輸入配對碼後點確定,都會立馬斷開連接。如果不輸入也不取消則會在30秒以內自動斷開藍牙打印機)

現在採用的方式是各給安卓和IOS寫一套藍牙打印的命令
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調用安卓原生系統API。
原生安卓文檔 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};

關於二維碼的打印
通過上面的文章我們可以知道

我們需要讀取生成後的二維碼的像素點的rgba,再將圖片數據先4合1判斷0還是1(0代表打印1代表不打印),緊接著八合1,因為一個字節有8位。最後使用打印機的位圖指令逐行掃描打印

4合1
本想著二維碼不是黑就是白,肯定不是255就是0,其實還是會有一小部分是其他數值的,這個要注意哦,每4位是一個像素點的rgba,然後黑白色的rgb就是(0,0,0)和(255,255,255),所以每四位只把第一位黑白化,然後將每四位的第一位取出來作為新的數組,當rule>200的時候,值取0,表示不打印,否則取1,表示打印;

8合1
假如我們取出來的8位數是[0,0,0,0,0,0,0,1],這個時候8合1,我們需要進行進制轉換,從右往左是2的零次方,2的一次方,等等,依次上加,實際是 0 * 27 + 0 * 26 + 0 * 25 + 0 * 24 + 0 * 23 + 0 * 22 + 0 * 21 + 1 * 20,這個數就是我們要的最終數據的其中之一。

將數據轉換成ArrayBuffer,其次打印必須要有指令!參考網址以及標準的ESC-POS指令集,下面代碼中的數字都是指令,另外,由於我這邊的打印機支持的是gb2312格式,所以在轉成ArrayBuffer的同時,還需要把編碼格式轉成正確的格式。

不過有一點我是要說下的,要注意ios和安卓的不同,安卓一次只能寫入不超過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,然後畫圖和讀圖的數據一致

相關函數
(經過反復測試得出,打印紙一行最大字節數是32字節,這裡指的是普通的票據打印機)
打印三列或者兩列,是需要自己計算空格進行填充,沒有現成的指令噢
總寬度 - 左側文字長度 - 右側文字長度 就是空格的長度。

/** * 打印兩列 * * @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,既是字符串總的字節長度 }



代碼大多是直接從項目中copy過來的,沒有整理過,並不能直接運行,僅供參考
不是我那一卷打印失敗的打印紙丟了,不然就讓你們看看什麼叫做 第一次做打印機開發的程序員這次是真大乾貨,篇幅也很長,辛苦了我

Powered by ❤️ with Hugo and Stack Theme.