uni-app(android、ios) 使用蓝牙便携式打印机(热敏打印机)

机型等参数

  • HSPOS
  • 点密度:576点/行(8dots/mm,203dpi)
  • 接口类型: 蓝牙(Bluetooth2.0,4.0双模,支持Android,IOS)
  • 打印方式:图形打印(位图)
  • 打印指令集: ESC/POS

基本思路

1、 实现蓝牙连接

**B12.js方法封装

class BluetoothTools {
	constructor() { 
		this.decimalData = []; //获取buffer之前的二进制数据集合;
		this.deviceId = null;
		this.initStatus = 0; //蓝牙初始化结果;0:初始化中,1-成功;2-失败;
		this.linked = false; //蓝牙是否为连接状态;
		this.connectChangeHandler = null;
	}
	
	// 初始化;
	init(connectChangeHandler) {
		this.connectChangeHandler = connectChangeHandler;
		return new Promise((resolve, reject) => {
			uni.openBluetoothAdapter({
				success: res => {
					this.initStatus = 1;
					this.onConnectChange(connectChangeHandler)
					resolve(res)
				},
				fail: err => {
					this.initStatus = 2;
					console.log('蓝牙初始化失败:', err);
					this.toast('蓝牙不可用!')
					reject(err)
				}
			})
		})
	}
	// 获取蓝牙状态,是否可用
	checkCanUse() {
		return new Promise((resolve, reject) => {
			uni.getBluetoothAdapterState({
				success: res => {
					// console.log(res);
					resolve(res)
				},
				fail: err => {
					// console.log(err);
					reject(err)
				}
			})
		})
	} 
	// 开始搜索蓝牙
	startSearch(cb) {
		return new Promise(async (resolve, reject) => {
			// if (this.initStatus === 2) {
			// 	this.toast('蓝牙初始化失败!');
			// 	reject();
			// 	return false;
			// } else if (this.initStatus === 0) {
			// 	this.toast('蓝牙未初始化!');
			// 	reject();
			// 	return false;
			// }
			if (this.initStatus !== 1) {
				await this.init(this.connectChangeHandler);
			}

			this.onFound(cb);
			uni.startBluetoothDevicesDiscovery({
				allowDuplicatesKey: false,
				services: [],
				success: (res) => {
					// console.log('开始搜索蓝牙设备!', res);
					resolve(res)
				},
				fail: err => {
					// console.log('开启搜索失败:', err);
					this.toast('开始搜索失败!')
					reject(err)
				}
			})
		})
	}
	// 监听单一设备搜索结果
	onFound(cb) {
		uni.onBluetoothDeviceFound(res => {
			// console.log('蓝牙设备:', res.devices);
			cb(res.devices) 
		})
	}
	
	// 停止搜索;
	stopSearch() {
		return new Promise((resolve, reject) => {
			uni.stopBluetoothDevicesDiscovery({
				success: (res) => {
					resolve(res)
				},
				fail: (err) => {
					// this.toast('蓝牙停止搜索失败,请重试!');
					console.log('蓝牙停止搜索失败:', err);
					reject(err)
				}
			})
		})
	}
	// 关闭蓝牙模块(打印完后调用)
	closeAdapter() {
		return new Promise((resolve, reject) => {
			uni.closeBluetoothAdapter({
				success: res => {
					resolve(res);
				},
				fail: (err) => {
					console.log('关闭失败:', err);
					// this.toast('蓝牙模块关闭失败!')
					reject(err);
				}
			})
		})
	}
	toast(msg) {
		uni.showToast({
			title: msg,
			icon: 'none',
			duration: 2200
		})
	}
	// 根据deviceId连接蓝牙;
	connectBLE(deviceId) {
		return new Promise(async (resolve, reject) => {
			if (this.initStatus !== 1) {
				await this.init(this.connectChangeHandler).catch(err => {
					reject(err)
				});
			}
			this.checkCanUse().then(async data => {
				// console.log(data);
				if (data.available) {
					uni.createBLEConnection({
						deviceId: deviceId,
						success: (res) => {
							resolve(res)
						},
						fail: err => {
							console.log('蓝牙连接失败!', err);
							this.toast('蓝牙连接失败,请重试!')
							reject(err)
						}
					})
				} else {
					this.toast('蓝牙不可用')
					reject()
				}
			}).catch(err => {
				this.toast('蓝牙不可用!!')
				reject()
			})
		})
	}

	//  根据deviceId断开蓝牙链接;
	closeBLE(deviceId) {
		return new Promise((resolve, reject) => {
			if (!deviceId) {
				resolve();
				return;
			}
			uni.closeBLEConnection({
				deviceId: deviceId,
				success: (res) => {
					// console.log('蓝牙已断开!', res);
					resolve(res)
				},
				fail: err => {
					console.log('蓝牙断开失败!', err);
					// this.toast('蓝牙断开失败, 请重试!')
					reject(err)
				}
			})
		})
	}
	// 监听蓝牙连接状态;
	onConnectChange(cb) {
		uni.onBLEConnectionStateChange(res => {
			console.log('监听蓝牙连接状态:', res);
			if (res.connected) {
				this.linked = true;
				this.deviceId = res.deviceId;
			} else {
				this.linked = false;
				this.deviceId = null;
			}
			cb(res);
		})
	}
}

export default BluetoothTools;

*** vue文件中内容

<template>
	<view class="">
		<view class="bluetooth_container">
			<view class="title">
				已配对设备
			view>
			<view class="matched_list" v-if="matchedList && matchedList.length">
				<view class="flex" v-for="(item, index) in matchedList" :key="item.deviceId" @longpress="showModal2(item.deviceId)">
					<image class="img" src="/static/print.png" mode="">image>
					<text class="name">{{item.localName ? item.localName : item.name ? item.name : '--'}}text>
					<text class="link linked" @click="showModal" v-if="linkedDeviceId === item.deviceId">已连接text>
					<block v-else>
						<image v-if="selectedDeviceId==item.deviceId && isConnectting" class="load_img" src="/static/loadding.gif" mode="">image>
						<text v-else class="link unlink" @click="connectHandler(item)">未连接text>
					block>
				view>
			view>
			<view class="empty_box" v-else>
				<u-empty mode="search" text="暂无配对设备,快快添加吧~">u-empty>
			view>
			<view class="block">view>
			<view class="title search_title">
				<text>扫描可用设备text>
				<view class="stop" v-if="isSearching" @click="stopSearch">
					<text>停止text>
				view>
				<view class="img_box" v-else @click="startSearch">
					<image class="img img1" src="/static/refresh.png" mode="">image>
				view>
				<view class="load_box" v-if="isSearching">
					<image class="load_img" src="/static/loadding.gif" mode="">image>
				view>
			view>
			<view class="list">
				<view class="flex" v-for="(item, index) in deviceList" :key="index">
					<image class="img img2" src="/static/print.png" mode="">image>
					<text class="name">{{item.localName ? item.localName : item.name ? item.name : '--'}}text>
					<image v-if="selectedDeviceId==item.deviceId && isConnectting" class="load_img" src="/static/loadding.gif" mode="">image>
					<text v-else class="link" @click="connectHandler(item,index)">连接text>
				view>
			view>
		view>
		
		<u-modal v-model="modalShow" show-cancel-button content="是否断开连接?" @confirm="closeConnect">u-modal>
		<u-modal v-model="modalShow2" show-cancel-button content="是否忽略此设备?" @confirm="deleteDevice">u-modal>

	view>
template>

<script>
	import B12s from '@/common/b12s.js';
	const b12s = new B12s();
	export default {
		data() {
			return {
				// 蓝牙相关;
				matchedList: [], //已配对的列表;
				deviceList: [], //搜索到的设备列表;
				initCode: 0, //蓝牙初始化结果;0:初始化中,1-成功;2-失败;
				selectedDeviceId: '', //当前操作的蓝牙设备id; 
				isConnectting: false, //蓝牙正在连接中;
				linkedDeviceId: '', //已连接的蓝牙mac地址;
				isSearching: false, //是否正在搜索蓝牙设备;

				b12s: null, //蓝牙工具;
			}
		},
		async onLoad(options) {
			this.initBluetooth();
		},
		async onUnload() {
			this.stopSearch();
			b12s.closeBLE(this.linkedDeviceId);
			b12s.closeAdapter();
		},
		methods: {
			// 初始化蓝牙;
			async initBluetooth() {
				this.getMatchedList();
				await b12s.init(this.onConnectChange);
				this.autoConnect();
			},
			// 如果有配对历史,自动连接最后一个;
			autoConnect() {
				let length = this.matchedList.length
				if (length <= 0) return;
				let item = this.matchedList[length - 1];
				this.connectHandler(item);
			},
			// 开始搜索蓝牙;
			async startSearch() {
				await b12s.startSearch(this.getDeviceList);
				this.isSearching = true;
			},
			// 停止搜索
			async stopSearch() {
				await b12s.stopSearch();
				this.isSearching = false;
			},
			// 获取历史配对列表;
			getMatchedList() {
				let list = uni.getStorageSync('__bluetooth_list_');
				if (list) {
					this.matchedList = list;
				}
				console.log(this.matchedList);
			},
			// 获取可用设备列表;
			getDeviceList(devices) {
				for (let i = 0; i < devices.length; i++) {
					let name = devices[i].name || devices[i].localName;
					if (name) {
						let dLIndex = this.deviceList.findIndex(item => item.deviceId === devices[i].deviceId);
						let mLIndex = this.matchedList.findIndex(item => item.deviceId === devices[i].deviceId);
						if (dLIndex < 0 && mLIndex < 0) {
							this.deviceList.push({
								name: name,
								deviceId: devices[i].deviceId
							})
						}
					}
				}
			},
			// 点击连接;
			async connectHandler(item, index = -1) {
				this.stopSearch();
				await b12s.closeBLE(this.linkedDeviceId)
				this.selectedDeviceId = item.deviceId;
				this.isConnectting = true;
				await b12s.connectBLE(item.deviceId).catch(err => {
					this.isConnectting = false;
				});
				// this.linkedDeviceId = item.deviceId;
				this.isConnectting = false;
				let indexRes = this.matchedList.findIndex(itm => itm.deviceId === item.deviceId);
				if (indexRes < 0) {
					this.matchedList.push(item);
					index !== -1 && this.deviceList.splice(index, 1);
					this.saveStorage()
				}
			},
			// 断开连接
			closeConnect() {
				b12s.closeBLE(this.linkedDeviceId);
				this.modalShow = false;
			},
			// 监听蓝牙连接状态;
			onConnectChange(res) {
				if (res.connected) {
					// this.$u.toast('蓝牙已连接');
					this.linkedDeviceId = res.deviceId;
				} else {
					// this.$u.toast('蓝牙已断开');
					this.linkedDeviceId = '';
				}
			},
			// 删除已缓存设备;
			deleteDevice() {
				this.matchedList = this.matchedList.filter(item => item.deviceId !== this.selectedDeviceId);
				this.saveStorage()
			},
			// 缓存已配对蓝牙;
			saveStorage() {
				uni.setStorageSync('__bluetooth_list_', this.matchedList)
			}
		}
	}
script>

<style lang="scss" scoped>

	.bluetooth_container {
		padding: 30rpx;

		.load_img {
			width: 40rpx;
			height: 40rpx;
		}

		.title {
			font-size: 34rpx;
			font-weight: bold;

			.img {
				margin-left: 20rpx;
				vertical-align: middle;
				width: 36rpx;
				height: 36rpx;
			}
		}

		.search_title {
			display: flex;
			justify-content: space-between;
			margin-top: 60rpx;

			.img_box {
				flex: 1;
			}

			.load_img {
				width: 40rpx;
				height: 40rpx;
			}

			.stop {
				flex: 1;
				margin-left: 30rpx;

				text {
					font-size: 24rpx;
					font-weight: normal;
					background-color: #fa3534;
					color: #ffffff;
					padding: 10rpx 20rpx;
					border-radius: 30rpx;
				}
			}
		}

		.matched_list {
			// padding: 30rpx 0;

			.flex {
				display: flex;
				margin: 30rpx 0;
				justify-content: space-between;
				align-items: center;

				.img {
					width: 40rpx;
					height: 40rpx;
				}

				.name {
					flex: 1;
					margin-left: 40rpx;
				}

				.link {
					padding: 10rpx 40rpx;
					background-color: #F2F2F2;
					border-radius: 40rpx;
					font-size: 28rpx;
				}

				.unlink {
					color: #606266;
				}

				.linked {
					color: #19be6b;
				}
			}
		}

		.empty_box {
			padding: 90rpx 0;
		}

		.list {
			.flex {
				display: flex;
				margin: 30rpx 0;
				justify-content: space-between;
				align-items: center;

				.img {
					width: 40rpx;
					height: 40rpx;
				}

				.name {
					flex: 1;
					margin-left: 40rpx;
				}

				.link {
					padding: 10rpx 40rpx;
					background-color: #F2F2F2;
					border-radius: 40rpx;
					color: #2979ff;
					font-size: 28rpx;
				}
			}
		}

		.btns {
			display: flex;
			justify-content: space-around;
			margin-top: 200rpx;

			.btn {
				margin: 0;
				width: 40%;
			}
		}
	}

style>

2、获取位图信息

vue页面中拿到像素(位图)信息;

<template>
	<view class="">
		<canvas id="canvas" canvas-id="canvas" class="canvas">canvas>
	view>
template>

<script>
	import B12s from '@/common/b12s.js';
	const b12s = new B12s();
	export default {
		methods: {
			//画图;
			drawImage() {
				const ctx = uni.createCanvasContext('canvas');
				uni.chooseImage({
				  success: res => {
					ctx.drawImage(res.tempFilePaths[0], 0, 0, 150, 100)
					ctx.draw()
				  }
				})
			},
			// 获取canvas的像素信息;
			getImageInfo() {
				return new Promise((resolve, reject) => {
					uni.canvasGetImageData({
						canvasId: 'canvas',
						x: 0,
						y: 0,
						width: _this.canvas2ImgWidth, 
						height: _this.canvas2ImgHeight,
						success(res) {
							resolve(res)
						},
						fail: err => {
							this.$u.toast('获取图片数据失败')
							reject(err)
						}
					})
				})
			},
			async printHandler() {
				uni.hideLoading();
				uni.showLoading({
					title: '打印中',
					mask: true
				})
				const res = await this.getImgInfo();
				b12s.printImage(res)
			}
		}
	}
script>

3、开始打印;

b12s.js

class BluetoothTools {
	constructor() {
		this.commands = {
			init: [0x1b, 0x40], // 清理buffer数据,重置模式;ASC2: ESC @
			print: [0x0a], //开始打印;
			printL5: [0x1b, 0x64, 0x05],
			printL6: [0x1b, 0x64, 0x06],
			printL8: [0x1b, 0x64, 0x08],
			printL10: [0x1b, 0x64, 0x10],
		}
		this.decimalData = []; //获取buffer之前的二进制数据集合;
		this.deviceId = null;
		this.initStatus = 0; //蓝牙初始化结果;0:初始化中,1-成功;2-失败;
		this.linked = false; //蓝牙是否为连接状态;
		this.timer = null;
		this.writeTime = 0;
		this.byteLength = this.isAndroid() ? 200 : 500;
		this.connectChangeHandler = null;
	}
	/**
	 * 低功耗蓝牙API
	 */
	// 根据deviceId 设置传输字节最大值;
	setMTU(deviceId) {
		return new Promise((resolve, reject) => {
			uni.setBLEMTU({
				deviceId: deviceId,
				mtu: 512,
				success: (res) => {
					console.log('设置MTu成功', res);
					resolve(res)
				},
				fail: (err) => {
					console.log('设置mtu失败', err);
					this.toaset('设置mtu失败!')
					reject(err)
				}
			})
		})
	}
	// 根据deviceId/mac地址 查询serviceId列表;
	getServiceId(deviceId) {
		return new Promise((resolve, reject) => {
			uni.getBLEDeviceServices({
				deviceId: deviceId,
				success: res => {
					resolve(res)
				},
				fail: err => {
					console.log('获取serviceId失败', err);
					reject(err)
				}
			})
		})
	}
	// 根据deviceId 和 serviceId 获取特征Id;
	getCharId(deviceId, serviceId) {
		return new Promise((resolve, reject) => {
			uni.getBLEDeviceCharacteristics({
				deviceId: deviceId,
				serviceId: serviceId,
				success: res => {
					resolve(res)
				},
				fail: err => {
					console.log('获取特征id失败', err);
					reject(err)
				}
			})
		})
	}

	// 写入数据;
	writeBLE(deviceId, serviceId, charId, buffer) {
		// console.log(deviceId, serviceId, charId, buffer.byteLength);
		// return;
		let _this = this;
		return new Promise((resolve, reject) => {
			uni.writeBLECharacteristicValue({
				deviceId: deviceId, //设备Id、mac地址;
				serviceId: serviceId, //服务id;
				characteristicId: charId, //特征id;
				value: buffer, //指令buffer数据;
				writeType: 'write', //'writeNoResponse',
				success: res => {
					// console.log('数据写入成功', res);
					_this.writeTime = 0;
					resolve(res)
				},
				fail: err => {
					console.log('写入失败:', Date.now());
					reject(err);
				}
			})
		})
	}
	

	/**
	 * 具体功能实现
	 */
	// 打印传入的位图信息
	async printImage(res) {
		if (!this.deviceId) {
			this.toast('未检测到蓝牙设备id');
			this.hideLoading()
			return;
		}
		this.isAndroid() && await this.setMTU(this.deviceId);
		const imgArr = this.getImgArray(res);
		// return;
		const { serviceId, charId } = await this.getWriteIds(this.deviceId);
		this.startPrint(this.deviceId, serviceId, charId, imgArr);
	}
	// 开始打印;
	async startPrint(deviceId, serviceId, charId, cmd) {
		// 初始化;
		let initArr = Array.from(this.commands.init).concat(Array.from(this.commands.print));
		let initBuffer = new Uint8Array(initArr).buffer;
		await this.writeMidWare(deviceId, serviceId, charId, initBuffer)
		// let cmds = Array.from(this.commands.init).concat(Array.from(cmd)).concat(Array.from(this.commands.printL10));
		// 分别传输指令;
		let cmds = Array.from(cmd);
		console.log(cmds.length);
		if (cmds.length > this.byteLength) {
			let cmdArrs = [];
			let newCmds = Array.from(cmds);
			let length = Math.ceil(cmds.length / this.byteLength);
			for (let i = 0; i < length; i++) {
				cmdArrs.push(newCmds.slice(this.byteLength * i, this.byteLength * (i + 1)));
			}
			for (let i = 0; i < cmdArrs.length; i++) {
				console.log(i, cmdArrs.length);
				let buffer = new Uint8Array(cmdArrs[i]).buffer;
				await this.writeMidWare(deviceId, serviceId, charId, buffer)
			}
		} else {
			let buffer = new Uint8Array(cmds).buffer;
			cmds.length && await this.writeMidWare(deviceId, serviceId, charId, buffer)
		}
		// 打印空行;
		let printLCmd = Array.from(this.commands.printL5);
		let printLBuffer = new Uint8Array(printLCmd).buffer;
		await this.writeMidWare(deviceId, serviceId, charId, printLBuffer);
	}
	// 处理安卓写入失败的问题;
	writeMidWare(deviceId, serviceId, charId, buffer) {
		let _this = this;
		return new Promise((resolve, reject) => {
			this.writeBLE(deviceId, serviceId, charId, buffer).then(res => {
				resolve(res)
			}).catch(err => {
				// console.log(111111, _this.writeTime);
				if (_this.writeTime < 50) {
					_this.writeTime++;
					return new Promise((reso, reje) => {
						clearTimeout(this.timer)
						this.timer = setTimeout(() => {
							reso(this.writeMidWare(deviceId, serviceId, charId, buffer))
						}, 100)
					})
				} else {
					_this.writeTime = 0;
					_this.hideLoading();
					_this.toast('数据写入失败,请走纸后重试!');
					reject(err);
				}
			}).then(res => {
				resolve(res)
			})
		})
	}
	// 位图信息转换为打印指令;
	getImgArray(res) {
		var w = res.width;
		var width = parseInt((res.width + 7) / 8 * 8 / 8);
		var height = res.height;
		let data = [29, 118, 48, 0];
		// let data = [0x1d, 0x76, 0x30, 0];
		data.push(parseInt(width % 256));
		data.push(parseInt(width / 256));
		data.push(parseInt(res.height % 256));
		data.push(parseInt(res.height / 256));
		var bits = new Uint8Array(height * width);
		for (let y = 0; y < height; y++) {
			for (let x = 0; x < w; x++) {
				var color = res.data[(y * w + x) * 4 + 1];
				if (color > 128) {
					bits[parseInt(y * width + x / 8)] |= (0x80 >> (x % 8));
				}
			}
		}

		for (let i = 0; i < bits.length; i++) {
			data.push((~bits[i]) & 0xFF)
		}
		return data;
	}
	// 获取写入数据需要的ids;
	async getWriteIds(deviceId) {
		return new Promise(async (resolve, reject) => {
			let serviceIdData = await this.getServiceId(deviceId);
			let services = serviceIdData.services;
			// console.log(services);
			let charId,
				serviceId;
			for (let i = 0; i < services.length; i++) {
				const chars = await this.getCharId(deviceId, services[i].uuid);
				// console.log(services[i].uuid, chars);
				const charItem = chars.characteristics.filter(item => item.properties.write)[0];
				if (charItem) {
					charId = charItem.uuid;
					serviceId = services[i].uuid;
					// break;
					// console.log(charId, serviceId);
				}
			}
			resolve({ charId: charId, serviceId: serviceId })
		})
	}
	isAndroid() {
		return uni.getSystemInfoSync().osName === 'android'
	}
}

export default BluetoothTools;

1、安卓设备有数据限制,如果传输数据过多。虽然writeBLE回调成功,但是会丢数据。可能会导致打印错乱、打印乱码。有些机型会直接不打印。

2、安卓设备必须要用到 writeMidware中的循环写入数据方式。将初始化,写入数据、打印空行这三个步骤分开。

所有的安卓设备都可能会在写入数据的时候,写入失败。一般过200ms后重新写入可能会成功,本方法中,是按照正常方式循环写入数据。一旦失败后会延迟100ms写入数据,不断尝试最多50次。

waitting complete。。。

你可能感兴趣的:(uni-app,单片机,javascript)