Recently, I have been working on receipt printing. The project requirement is to implement it on both IOS and Android. I was confused when I started doing it. Then I found a lot of information on the Internet, stepped on a lot of pitfalls, and read a lot of articles. The result turned out to be okay. Bluetooth printers are generally divided into two printing modes, receipt printing and label printing.

The crappy printer the company bought didn’t even have development documentation, which caused me a lot of pitfalls. They didn’t even consult me ​​when I was asked to develop and buy it. Currently, there is no problem with the wx.createBLEConnection test of the WeChat applet connecting to a Bluetooth printer on IOS devices, but an exception will occur on some Android phones (shown as, the system pairing box will pop up when connecting, and whether you click cancel or enter the pairing code and click OK, the connection will be disconnected immediately. If you do not enter or cancel, the Bluetooth printer will be automatically disconnected within 30 seconds)

The current method is to write a set of Bluetooth printing commands for Android and IOS. IOS

// ====================Bluetooth operation================== //Initialize the Bluetooth module openBluetoothAdapter() {

if (app.sysinfo.provider == 1) {
    // Turn on Bluetooth
    app.onBluetooth()
    setTimeout(() => {
        this.android\_search()
    }, 2000)
    return false;
}


this.closeBluetoothAdapter()
uni.openBluetoothAdapter({
    success: (res) => {
        console.log("Initializing Bluetooth module: " + JSON.stringify(res));
        this.startBluetoothDevicesDiscovery()
    },
    fail: (res) => {
        if (res.errCode === 10001) {
            uni.onBluetoothAdapterStateChange((res) => {
                console.log('Listening to Bluetooth adapter status change events', 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('Initialization of Bluetooth failed, error code: ' + res.errCode)
            return false;
        }
        app.alert(res.errMsg)
    }
})

},

//Get the local Bluetooth adapter status getBluetoothAdapterState() { uni.getBluetoothAdapterState({ success: (res) => { console.log(‘Get the local Bluetooth adapter status.’, JSON.stringify(res)) if (res.discovering) { this.onBluetoothDeviceFound() } else if (res.available) { this.startBluetoothDevicesDiscovery() } }, fail: (res) => { console.log(’error: Failed to obtain local Bluetooth adapter status’, JSON.stringify(res)) setTimeout(() => { this.getBluetoothAdapterState() }, 500) } }) },

//Start searching for nearby Bluetooth peripherals startBluetoothDevicesDiscovery() { console.log(this.discoveryStarted); if (this.discoveryStarted) { return } console.log(‘Start searching for Bluetooth devices’); 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(‘The current Bluetooth adapter is unavailable’) } else { app.alert(‘Searching for Bluetooth failed, status code: ’ + res.errCode) } } }) }, 500)

},

// Stop searching stopBluetoothDevicesDiscovery() { uni.stopBluetoothDevicesDiscovery() this.discoveryStarted = false },

//Find the callback function for the event of new device onBluetoothDeviceFound() { console.log(‘Callback function for events finding new devices’); 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 }

    })
})

},

//Connect to low-power Bluetooth device createBLEConnection(e) { uni.showLoading({ title: ‘Device connecting’, 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('paired')
    }

    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('Detected that the device is not connected, try to connect....');
        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()
        // Prevent acquisition failure
        setTimeout(() => {
            this.getBLEDeviceServices(deviceId)
        }, 1000)
    },
    fail: (res) => {
        uni.hideLoading();
        app.Toast('Device connection failed')
        console.log("Bluetooth connection failed:", res);
    }
})
this.stopBluetoothDevicesDiscovery()

},

//Get all services of Bluetooth device (service) getBLEDeviceServices(deviceId) { uni.getBLEDeviceServices({ deviceId, success: (res) => { console.log(“Acquiring Bluetooth service successfully: " + JSON.stringify(res)) if (res.services.length == 0) { uni.hideLoading(); app.alert(‘Bluetooth service not obtained, unable to print 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(“Failed to obtain Bluetooth service: " + JSON.stringify(res)) } }) },

//Get all characteristic values ​​(characteristic) in a service of the Bluetooth device getBLEDeviceCharacteristics(deviceId, serviceId) { console.log(‘Get all characteristic values ​​in a service of the Bluetooth device’, deviceId, serviceId) uni.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) => { console.log(‘Get all characteristic values ​​in a service of the Bluetooth device success’, JSON.stringify(res)) uni.hideLoading(); if (res.characteristics.length == 0) { app.alert(‘Bluetooth service not obtained, unable to print 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('Failed to obtain characteristic value:', res)
    }
})

// Monitor before operating to ensure that data is obtained as soon as possible
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) => { // This method callback can be used to handle abnormal situations such as unexpected disconnection of connections. console.log(`Bluetooth connection status change device ${res.deviceId} state has changed, connected: ${res.connected}`) if (res.connected == false) { app.global_printing = {} this.connected = false this.chs = [] this.canWrite = false } }) },

//Disconnect from Bluetooth low energy device closeBLEConnection() { app.global_printing = {} uni.closeBLEConnection({ deviceId: this.deviceId }) this.connected = false this.chs = [] this.canWrite = false },

//Close the Bluetooth module closeBluetoothAdapter() { app.global_printing = {} uni.closeBluetoothAdapter() this.discoveryStarted = false },

//Send data 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(‘Data sent: ’ + bufferstr) // console.log(‘message sent successfully’) }, fail: function(res) { fail(res) console.log(“Data sending failed:” + JSON.stringify(res)) }, complete: function(res) { // console.log(“Sending completed:” + JSON.stringify(res)) } }) },

//Traverse and send data printCode(arr) { var that = this; if (arr. length > 0) { this.sendStr(arr[0], function(success) { arr.shift(); that.printCode(arr); }, function(error) { app.alert(‘Printing failed, error code: ’ + error.errCode) app.printing_status = false console.log(error); }); return false; }

setTimeout(function() {
    app.printing\_status = false
    console.log('End of printing');
}, 1000);

},

**Android **It is relatively simple and convenient, using Native.js to directly call the Native Java interface channel, and calling the Android native system API through plus.android.
Native Android documentation https://developer.android.google.cn/reference/android/bluetooth/BluetoothAdapter?hl=en

// ======================Android============ //Search for Bluetooth devices android_search(address = ‘’) { //Search and match 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(“Start searching for devices”); var filter = new IntentFilter(); var bdevice = new BluetoothDevice(); var on = null; var un = null; console.log(‘Searching please wait’); BAdapter.startDiscovery(); //Start search var receiver; receiver = plus.android.implements(‘io.dcloud.android.content.BroadcastReceiver’, { onReceive: (context, intent) => { //Implement onReceiver callback function plus.android.importClass(intent); //Introduce the intent class through the intent instance to facilitate future ‘.’ operations // console.log(intent.getAction()); //Get action if (intent.getAction() == “android.bluetooth.adapter.action.DISCOVERY_FINISHED”) { main.unregisterReceiver(receiver); //Cancel listening console.log(“Search ended”) } else { var BleDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); //Judge whether matching if (BleDevice.getBondState() == bdevice.BOND_NONE) { console.log(“Unpaired Bluetooth device: " + BleDevice.getName() + ’ ’ + BleDevice.getAddress()); //If the parameter is the same as the obtained mac address, match it if (address == BleDevice.getAddress()) { if (BleDevice.createBond()) { //Pairing command.createBond() if (BleDevice.getName() != null) { console.log(“Bluetooth device paired successfully: " + BleDevice.getName() + ’ ’ + BleDevice.getAddress()); // app.Toast(“Bluetooth device paired successfully:” + BleDevice.getName()) } } else { console.log(‘Pairing failed’) } } else { if (BleDevice.getName() != on) { //Judge to prevent repeated addition on = BleDevice.getName(); if (BleDevice.getName() != null) { this.devices.push({ deviceId: BleDevice.getAddress(), name: BleDevice.getName() }) console.log(“Bluetooth device found: " + BleDevice.getName() + ’ ’ + BleDevice.getAddress()); } } } } else { if (BleDevice.getName() != un) { //Judge to prevent repeated addition un = BleDevice.getName(); if (BleDevice.getName() != null) { this.devices.push({ deviceId: BleDevice.getAddress(), name: BleDevice.getName() + ’ (paired)’, pair: true }) console.log(“Paired Bluetooth device: " + 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); //Register listening

},

// Print android_printCode(arr) {

var that = this;

// Print
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('Print failed')
    return false;
}


if (!bluetoothSocket.isConnected()) {
    console.log('Detected that the device is not connected, try to connect....');
    bluetoothSocket.connect();
}

console.log('Device is connected');

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 //Key here
    bluetoothSocket.close(); //The Bluetooth connection must be closed otherwise an error will be printed if it is accidentally disconnected.
}

setTimeout(function() {
    app.printing\_status = false
    console.log('End of printing');
}, 1000);

},

Print command (For more printing commands, please refer to https://www.jianshu.com/p/dd6ca0054298)

/** * Reset printer */ public static final byte[] RESET = {0x1b, 0x40};

/** * left aligned */ public static final byte[] ALIGN_LEFT = {0x1b, 0x61, 0x00};

/** * center alignment */ public static final byte[] ALIGN_CENTER = {0x1b, 0x61, 0x01};

/** * right aligned */ public static final byte[] ALIGN_RIGHT = {0x1b, 0x61, 0x02};

/** * Select bold mode */ public static final byte[] BOLD = {0x1b, 0x45, 0x01};

/** *Cancel bold mode */ public static final byte[] BOLD_CANCEL = {0x1b, 0x45, 0x00};

/** * Double width and height */ public static final byte[] DOUBLE_HEIGHT_WIDTH = {0x1d, 0x21, 0x11};

/** * double width */ public static final byte[] DOUBLE_WIDTH = {0x1d, 0x21, 0x10};

/** * Double the height */ public static final byte[] DOUBLE_HEIGHT = {0x1d, 0x21, 0x01};

/** * Font is not enlarged */ public static final byte[] NORMAL = {0x1d, 0x21, 0x00};

/** * Set default line spacing */ public static final byte[] LINE_SPACING_DEFAULT = {0x1b, 0x32};

About printing QR code From the above article we can know

We need to read the rgba of the pixels of the generated QR code, and then 4-in-1 the image data to determine whether it is 0 or 1 (0 means printing, 1 means not printing), and then 8-in-1, because there are 8 bits in a byte. Finally, use the printer’s bitmap command to scan and print line by line.

4 in 1 I originally thought that the QR code would be either black or white, definitely either 255 or 0. In fact, there will still be a small part of other values. Please pay attention to this. Every 4 digits is a pixel of rgba, and the black and white rgb is (0,0,0) and (255,255,255), so only the first bit of every four bits is blackened, and then the first bit of every four bits is taken out as a new array. When rule>200, the value is 0, which means no printing, otherwise it is 1, which means printing;

8 in 1 If the 8-digit number we take out is [0,0,0,0,0,0,0,1], then 8 in 1, we need to perform base conversion. From right to left, it is 2 raised to the zero power, 2 raised to the first power, and so on. Adding up in order, the actual number is 0 * 27 + 0 * 26 + 0 * 25 + 0 * 24 + 0 * 23 + 0 * 22 + 0 * 21 + 1 * 20, this number is one of the final data we want.

Convert the data into ArrayBuffer, and then there must be instructions for printing! Referring to the website address and the standard ESC-POS instruction set, the numbers in the code below are instructions. In addition, since my printer supports the gb2312 format, when converting to ArrayBuffer, the encoding format also needs to be converted to the correct format.

But there is one thing I want to say. Pay attention to the difference between ios and Android. Android can only write no more than 20 bytes at a time (ios is not sure about the details, but it is 120 bytes visually). It is recommended to directly intercept the data data.slice(20, byteLength), call back again after successful printing, and print in a loop.

// QR code 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(() => { // Get image data 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’));

                // QR code
                for (let i = 0; i < buffer.byteLength; i = i + 120) {
                    that.arrPrint.push(buffer.slice(i, i + 120));
                }
                callback()
            }
        })
        // }, 3000);
    }
});

},

  1. toArrayBuffer is a component that needs to be installed, https://www.npmjs.com/package/to-array-buffer or you can use this writing method const buffer = new Uint8Array(Buffer.from(cmds, ‘gb2312’)).buffer;

  2. Pay attention to check whether your data is correct. If there is a problem with the data in the drawing, black blocks may be printed;

  3. Count the data! ! ! Count! ! Count! ! , for example, when I draw a picture, it is 160*160, and then I print the data splicing instructions [29, 118, 48, 0, 20, 0, 160, 0]. The 20 and 160 in this are calculated. Please refer to the article above to see the reason. It is probably 1:8, and then the data for drawing and reading the picture are consistent.

Related functions (After repeated testing, it has been concluded that the maximum number of bytes in one line of printing paper is 32 bytes. This refers to ordinary receipt printers) To print three or two columns, you need to calculate the spaces and fill them yourself. There are no ready-made instructions. The total width - the length of the left text - the length of the right text is the length of the space.

/** * print two columns * * @param leftText left text * @param rightText right text * @return */ printTwoData(leftText, rightText) { var sb = ’’ var leftTextLength = this.getBytesLength(leftText); var rightTextLength = this.getBytesLength(rightText); sb += leftText

// Calculate the space between the text on both sides
var marginBetweenMiddleAndRight = 32 - leftTextLength - rightTextLength;

for (var i = 0; i < marginBetweenMiddleAndRight; i++) {
    sb += ' '
}
sb += rightText
return sb.toString();

}, /** * print three columns * * @param leftText left text * @param middleText middle text * @param rightText right text * @return */ printThreeData(leftText, middleText, rightText) { var sb = ’’ // Display up to 8 Chinese characters + two dots on the left 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
// Calculate the space length of the left text and the middle text
var marginBetweenLeftAndMiddle = 20 - leftTextLength - middleTextLength / 2;

for (var i = 0; i < marginBetweenLeftAndMiddle; i++) {
    sb += ' '
}
sb += middleText

// Calculate the space length of the text on the right and the text in the middle
var marginBetweenMiddleAndRight = 12 - middleTextLength / 2 - rightTextLength;

for (var i = 0; i < marginBetweenMiddleAndRight; i++) {
    sb += ' '
}

sb += rightText

// When printing, I found that the rightmost text is always one character to the right, so a space needs to be deleted.
// 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 in 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 in 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 to 16 progress string example ab2hex(buffer) { var hexArr = Array.prototype.map.call( new Uint8Array(buffer), function(bit) { return (‘00’ + bit.toString(16)).slice(-2) } ) return hexArr.join(’’); },

// Calculate the length of text occupied getBytesLength(str) { var num = str.length; //First use num to save the length of the string (can be understood as: first assume that each character only occupies one byte) for (var i = 0; i < str.length; i++) { //Traverse the string if (str.charCodeAt(i) > 255) { //Determine whether a character occupies two bytes. If so, num+1 num++; } } return num; //Return the final num, which is the total byte length of the string }

Most of the code is copied directly from the project. It has not been organized and cannot be run directly. It is for reference only. It’s not that I lost the roll of printing paper that failed to print, otherwise I would like to show you what it means. A programmer who is doing printer development for the first time has really great information this time, and the length is also very long. Thank you for your hard work.