前言
这是个蓝牙打印图片的功能,业务是打印界面固定的demo范围,这里通过html2canvas插件生成的图片base64,然后图片base64绘制到canvas中去后,获取canvas中的像素信息,然后对像素信息进行一个灰度值处理,灰度值处理后再进行黑白值的处理,然后再根据蓝牙机需要通过图片的宽高比例进行一个二维数组的生成等等,一番数据处理后,将数据转换为buffer格式 ,因为蓝牙一次只能发20字节所以采用递归方式发送。
采用贴纸打印的话,需要使用黑标指令发送固定数据给蓝牙一次,打印机会蜂鸣,目的是打印内容完后,自动切换下一个纸准备,不会仍旧在当前纸打印。
这里用的是南方鸿志科技的58mm热敏打印机,
总结:打印的view视图范围中有文字有图片,其中图片是base64格式是正常运行的,如果是本地图片会发现html2canvas在app端无法处理。
搜索连接蓝牙界面
javascript
<template>
<view class="content">
<button type="default" @click="bluetoothInit">搜寻蓝牙设备</button>
<button @click="goPrint">跳转打印界面</button>
<uni-search-bar :focus="true" v-model="searchValue" @input="input"
@cancel="cancel" @clear="clear">
</uni-search-bar>
<view class="list">
<view class="item" v-for="(item,index) in bluetoothList" :key="index">
<text class="name">
{{item.name}}----{{item.deviceId}}
</text>
<view class="btns" @click="connect(item)">点击连接</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
bluetoothList: [], //蓝牙列表
searchValue:""
}
},
onshow() {
},
created() {
},
methods: {
input(e){
console.log(e);
let bluetoothList=uni.getStorageSync('bluetoothList');
this.bluetoothList=bluetoothList.filter(element=>{
return element.name.includes(e)
})
console.log(this.bluetoothList);
},
cancel(){
this.bluetoothList=uni.getStorageSync('bluetoothList');
},
clear(){
this.bluetoothList=uni.getStorageSync('bluetoothList');
},
goPrint(){
uni.navigateTo({
url:'/pages/Print/Print'
})
},
//点击连接设备
connect(device) {
uni.showModal({
title: device.name,
content: '确定连接此设备?',
success: (res => {
if (res.confirm) {
uni.setStorageSync("DeviceID", device.deviceId) //把已经连接的蓝牙设备信息放入缓存
this.DeviceID = device.deviceId
let DeviceID = device.deviceId //这里是拿到的uuid
this.StopBluetoothDevicesDiscovery() //当找到匹配的蓝牙后就关掉蓝牙搜寻,因为蓝牙搜寻很耗性能
console.log("匹配到的蓝牙this.DeviceID:", this.DeviceID)
this.CreateBLEConnection(DeviceID) //创建蓝牙连接,连接低功耗蓝牙设备
uni.showLoading({
title: '正在尝试连接蓝牙...'
});
console.log('用户点击确定');
} else if (res.cancel) {
console.log('用户点击取消');
}
})
});
},
//蓝牙初始化
bluetoothInit() {
this.searchValue='';
this.bluetoothList = [];
uni.openBluetoothAdapter({
success: (res) => {
console.log('第一步初始化蓝牙成功:' + res.errMsg);
// 初始化完毕开始搜索
this.StartBluetoothDeviceDiscovery()
},
fail: (res) => {
console.log('初始化蓝牙失败: ' + JSON.stringify(res));
if (res.errCode == 10001) {
uni.showToast({
title: '蓝牙未打开',
duration: 2000,
})
} else {
uni.showToast({
title: res.errMsg,
duration: 2000,
})
}
}
});
},
/**
* 第二步 在页面显示的时候判断是都已经初始化完成蓝牙适配器若成功,则开始查找设备
*/
StartBluetoothDeviceDiscovery() {
uni.startBluetoothDevicesDiscovery({
// services: ['0000FFE0'],
success: res => {
console.log('第二步 开始搜寻附近的蓝牙外围设备:startBluetoothDevicesDiscovery success', res)
this.OnBluetoothDeviceFound();
},
fail: res => {
uni.showToast({
icon: "none",
title: "查找设备失败!",
duration: 3000
})
}
});
},
/**
* 第三步 发现外围设备
*/
OnBluetoothDeviceFound() {
console.log("监听寻找新设备");
uni.showLoading({
title: '搜寻设备中...'
});
uni.onBluetoothDeviceFound(res => {
console.log("第三步 监听寻找到新设备的事件:", JSON.stringify(res))
console.log("第三步 监听寻找到新设备列表:", res.devices)
let bluetoothList = [...res.devices, ...this.bluetoothList];
this.bluetoothList=bluetoothList;
uni.setStorageSync('bluetoothList', bluetoothList);
console.log(bluetoothList);
// res.devices.forEach(device => { //这一步就是去筛选找到的蓝牙中,有没有你匹配的名称
// console.log("这一步就是去筛选找到的蓝牙中,有没有你匹配的名称:", JSON.stringify(device))
// if (device.name == 'Qsprinter') { //匹配蓝牙名称
// uni.setStorageSync("DeviceID", device.deviceId) //把已经连接的蓝牙设备信息放入缓存
// this.DeviceID = device.deviceId
// let DeviceID = device.deviceId //这里是拿到的uuid
// this.StopBluetoothDevicesDiscovery() //当找到匹配的蓝牙后就关掉蓝牙搜寻,因为蓝牙搜寻很耗性能
// console.log("匹配到的蓝牙this.DeviceID:", this.DeviceID)
// this.CreateBLEConnection(DeviceID) //创建蓝牙连接,连接低功耗蓝牙设备
// }
// })
setTimeout(() => {
this.StopBluetoothDevicesDiscovery()
}, 10000)
});
},
/**
* 第四步 停止搜索蓝牙设备
*/
StopBluetoothDevicesDiscovery() {
uni.stopBluetoothDevicesDiscovery({
success: res => {
console.log("第四步 找到匹配的蓝牙后就关掉蓝牙搜寻:", JSON.stringify(res))
},
fail: res => {
console.log('第四步 停止搜索蓝牙设备失败,错误码:' + res.errCode);
},
complete() {
uni.hideLoading();
}
});
},
// 第五步 创建蓝牙连接,连接低功耗蓝牙设备
CreateBLEConnection(DeviceID, index) {
let doc = this
uni.createBLEConnection({ //创建蓝牙连接,连接低功耗蓝牙设备
deviceId: DeviceID, //传入刚刚获取的uuid
success(res) {
console.log("第五步 创建蓝牙连接成功:", JSON.stringify(res))
doc.GetBLEDeviceServices(DeviceID) //获取蓝牙设备所有服务(service)。
},
fail(res) {
console.log(res)
}
})
},
//第六步 获取蓝牙设备所有服务(service)。
GetBLEDeviceServices(DeviceID, index) {
let doc = this
setTimeout(function() { //这里为什么要用setTimeout呢,等等下面会解释
uni.getBLEDeviceServices({ //获取蓝牙设备所有服务
deviceId: DeviceID,
success(res) { //为什么要用延时,因为不用延时就拿不到所有的服务,在上一步,连接低功耗蓝牙
//设备的时候,需要一个600-1000毫秒的时间后,再去获取设备所有服务,不给延时就会一直返回错误码10004
console.log("第六步 获取蓝牙设备所有服务:", JSON.stringify(res))
uni.setStorageSync("ServiceUUID", res.services[2].uuid) //把已经连接的蓝牙设备信息放入缓存
uni.setStorageSync("ServiceUUIDNew", res.services[2].uuid) //把已经连接的蓝牙设备信息放入缓存
let ServiceUUIDNew = res.services[2].uuid
this.ServiceUUID = res.services[2].uuid
console.log("this.ServiceUUID:", this.ServiceUUID);
doc.GetBLEDeviceCharacteristics(DeviceID) //获取蓝牙设备某个服务中所有特征值
},
fail(res) {
console.log(JSON.stringify(res))
}
})
}, 1000)
},
// 第七步 获取蓝牙特征值
GetBLEDeviceCharacteristics(DeviceID) {
console.log("第七步 获取蓝牙特征值DeviceID:", DeviceID, "serviceId:", uni.getStorageSync('ServiceUUIDNew'));
setTimeout(() => {
let that = this;
uni.getBLEDeviceCharacteristics({ //获取蓝牙设备某个服务中所有特征值
deviceId: DeviceID,
serviceId: uni.getStorageSync('ServiceUUIDNew'), //这个serviceId可以在上一步获取中拿到,也可以在
//蓝牙文档中(硬件的蓝牙文档)拿到,我这里是通过文档直接赋值上去的,一般有两个,一个是收的uuid,一个是发的uuid,我们这边是发
success(res) {
console.log("第七步 获取蓝牙设备某个服务中所有特征值成功:", JSON.stringify(res))
uni.showToast({
title: '设备蓝牙已连接',
duration: 2000
});
// uni.hideLoading();
// #ifdef APP-IOS
uni.setStorageSync("CharacteristicId", res.characteristics[0]
.uuid) //把某个服务中所有特征值信息放入缓存
that.characteristicId = res.characteristics[0].uuid
console.log(res);
// #endif
// #ifdef APP-ANDROID
uni.setStorageSync("CharacteristicId", res.characteristics[1]
.uuid) //把某个服务中所有特征值信息放入缓存
that.characteristicId = res.characteristics[1].uuid
// #endif
// that.WriteBLECharacteristicValue()
},
fail(res) {
console.log("获取蓝牙设备某个服务中所有特征值失败:", JSON.stringify(res))
}
})
}, 2000)
},
}
}
</script>
<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.row {
display: flex;
}
.cell {
width: 10px;
height: 10px;
margin-top: 2rpx;
color: #000;
/* 这里可以根据灰度值设置背景色 */
}
.list {
margin-top: 10rpx;
}
.list .item {
width: 97%;
height: 100rpx;
margin-top: 10rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #ccc;
}
.btns {
white-space: nowrap;
}
</style>
蓝牙打印界面
javascript
<template>
<view>
<PrintCode ref='PrintCode' :imgUrl="imgUrl" codeType="code" :info="info"></PrintCode>
<button type="default" @click="btn">选中图片打印</button>
<button type="default" @click="btn2">黑标打印</button>
</view>
</template>
<script>
export default {
data() {
return {
imgUrl:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQAAAABYmaj5AAAAtElEQVR4nJWTwYkDQRADa43/tRk4/7CcgSYC3cOfgwMfml/DCKmlFoA2FODB7/dtwlYCtR1wD64bXxGue8A9Ad7e5950PgFenHhPOGyNcdzvAgTgbHxtW2vbZtEZkJC68aVRsbLwJZiIyeILDSmCdcshmBA3Xz5Ak7jhwLSUZNTZFsm4H0IsMOXQtqaxzPtRVMYcqqTWue/AOde/P/9Mx5v6vta+9wC59r5/znvC8el7Rl9+AN59eFd5eY5PAAAAAElFTkSuQmCC",
resultArray:[],
info:{
name:"xxxxxxxxxxxxxxxxxx",
model:"1111111111112123123132312",
amount:"12312312313212312123",
start_use_date:'2024-2-3'
}
}
},
methods: {
btn(){
this.$refs.PrintCode.open()
},
btn2(){
let value=[31, 27, 31, 128, 4, 5, 6, 68];
uni.writeBLECharacteristicValue({
deviceId: uni.getStorageSync('DeviceID'),
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId: uni.getStorageSync('ServiceUUIDNew'),
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId: uni.getStorageSync('CharacteristicId'),
// 这里的value是ArrayBuffer类型
value: value,
success: function(res) {
console.log(res);
//写入成功后继续递归调用发送剩下的数据
// that.sendMsg(newData)
},
fail: function(err) {
console.log(err)
},
complete: function() {}
})
},
},
onShow() {
uni.onBLEConnectionStateChange(function (res) {
// 该方法回调中可以用于处理连接意外断开等异常情况
console.log(`device ${res.deviceId} state has changed, connected: ${res.connected}`)
})
}
}
</script>
引用组件界面
html
<template>
<view>
<uni-popup ref="popup" type="center">
<view class="BigBox" v-if="codeType=='bar'">
<image :src="imgUrl" mode="" class="barImg"></image>
</view>
<view class="BigBox2" v-else id="pagePoster">
<view class="lf">
<view class="">
名称:{{setLength(info.name)}}
</view>
<view class="">
型号:{{setLength(info.model)}}
</view>
<view class="">
数量:{{setLength(info.amount)}}
</view>
<view class="">
投用时间:{{setLength(info.start_use_date)}}
</view>
</view>
<view class="rg">
<image :src="imgUrl" mode=""></image>
</view>
</view>
<view class="btnBox">
<view class="print" v-if="codeType=='bar'" @click="printBtn">
打印
</view>
<view class="print" v-else @click="canvasImage.generateImage">
打印
</view>
<view class="close" @click="close">
取消
</view>
</view>
<canvas canvas-id="myCanvas" id="myCanvas" :style="{width:imgWidth,height:imgHeight}"></canvas>
</uni-popup>
</view>
</template>
<script>
export default {
name: "PrintCode",
props: {
imgUrl: {
default: ""
},
codeType: {
default: 'bar'
},
info: {
type: Object
}
},
data() {
return {
imgWidth: "",
imgHeight: ""
};
},
mounted() {
uni.openBluetoothAdapter({
success: (res) => {
console.log('第一步初始化蓝牙成功:' + res.errMsg);
uni.showToast({
title: '蓝牙已初始化',
duration: 1000
});
},
fail: (res) => {
console.log('初始化蓝牙失败: ' + JSON.stringify(res));
if (res.errCode == 10001) {
uni.showToast({
title: '蓝牙未打开',
duration: 2000,
})
} else {
uni.showToast({
title: res.errMsg,
duration: 2000,
})
}
}
});
},
methods: {
//canvas无法显示省略号,给文字添加省略号,
setLength(e) {
console.log(e);
let text = e;
if (e?.length > 8) {
text = e.substring(0, 8) + '...';
}
return text
},
open() {
this.$refs.popup.open('center')
},
close() {
this.$refs.popup.close()
},
//二维码打印
receiveSendData(val) {
const ctx = uni.createCanvasContext('myCanvas', this);
// 获取图片信息成功后绘制到 Canvas 上
ctx.drawImage(val, 0, 0, 350, 176);
// 获取绘制完成的图片数据
ctx.draw(false, () => {
this.imgWidth = 350 + 'px';
this.imgHeight = 176 + 'px';
// 将 Canvas 中的图片数据转换为灰度图像
uni.canvasGetImageData({
canvasId: 'myCanvas',
x: 0,
y: 0,
width: 350,
height: 176,
success: (res) => {
console.log(res);
const imageData = res.data;
console.log(imageData);
this.chuli(imageData)
},
fail: (error) => {
console.error('获取图片数据失败', error);
}
});
});
},
//条码打印
printBtn() {
// const dcRichAlert = uni.requireNativePlugin('Yunjinginc-Print')
// dcRichAlert.printBitmap1(0,this.imgUrl,true)
},
//处理像素为打印机发送数据
chuli(imageData) {
let imgWidth = 350;
let imgHeight = 176;
// 将彩色图片转换为灰度图片
for (let i = 0; i < imageData.length; i += 4) {
const r = imageData[i];
const g = imageData[i + 1];
const b = imageData[i + 2];
// 根据灰度公式将 RGB 值转换为灰度值
const grayscale = 0.299 * r + 0.587 * g + 0.114 *
b;
// 将灰度值赋给 RGB
imageData[i] = grayscale; // Red
imageData[i + 1] = grayscale; // Green
imageData[i + 2] = grayscale; // Blue
}
console.log(imageData);
let blackWhiteData = this.convertToBlackWhite(
imageData); // 转换成黑白像素点数据
console.log(blackWhiteData);
this.grayscaleArray = [];
// 将一维数组转换为二维数组
for (let i = 0; i < imgWidth; i++) {
// 每一行的起始索引
let startIndex = i * imgWidth;
// 每一行的灰度值数据
let row = blackWhiteData.slice(startIndex,
startIndex + imgWidth);
// 将当前行添加到二维数组中
this.grayscaleArray.push(row);
}
let originalArray = this.grayscaleArray;
console.log(this.grayscaleArray);
let resultArray = this.complementArr(this
.grayscaleArray, imgWidth, imgHeight);
//求二维数组进行24行处理时除数与余数
const quotient = resultArray.length / 24; // 求次数
let list = []
// 对前24*n行进行处理
for (let i = 0; i < quotient; i++) {
const startIndex = i * 24;
const endIndex = (i + 1) * 24;
const rowsToProcess = originalArray.slice(
startIndex, endIndex);
console.log(rowsToProcess);
console.log(startIndex, endIndex);
list.push(this.processRows(rowsToProcess))
}
console.log(list);
// 二维转一维
let list2 = list.flat()
console.log(list2);
this.resultArray = list2;
// 创建一个 Uint8Array 视图对象,将数组数据复制到该视图中
const typedArray = new Uint8Array(this.resultArray);
// 获取 typedArray 的 buffer 属性,即得到对应的 ArrayBuffer
const buffer = typedArray.buffer;
this.sendMsg(buffer)
},
//发送数据给蓝牙
sendMsg(buffer) {
var newData = buffer.slice(20)
var writeBuffer = buffer.slice(0, 20)
console.log(writeBuffer.length);
if (writeBuffer.byteLength < 20) {
console.log("Invalid data length. Data will not be sent.");
//纸打印完了之后进行切刀,(标签纸)
uni.writeBLECharacteristicValue({
deviceId: uni.getStorageSync('DeviceID'),
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId: uni.getStorageSync('ServiceUUIDNew'),
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId: uni.getStorageSync('CharacteristicId'),
// 这里的value是ArrayBuffer类型
value: [27, 35, 35, 67, 84, 71, 72, 48],
success: function(res) {
console.log(res);
},
})
return; // 数据无效,不发送
}
let that = this;
console.log(uni.getStorageSync('DeviceID'));
uni.writeBLECharacteristicValue({
deviceId: uni.getStorageSync('DeviceID'),
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId: uni.getStorageSync('ServiceUUIDNew'),
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId: uni.getStorageSync('CharacteristicId'),
// 这里的value是ArrayBuffer类型
value: writeBuffer,
writeType: 'write',
success: function(res) {
//写入成功后继续递归调用发送剩下的数据
that.sendMsg(newData)
},
fail: function(err) {
console.log(err)
},
complete: function() {}
})
},
//补充数组
complementArr(originalArray, width, height) {
let originalArrayData = originalArray
let replenish = 24 - height % 24
for (let i = 0; i < replenish; i++) {
let arr = []
for (let j = 0; j < width; j++) {
arr.push(0)
}
originalArrayData.push(arr)
}
return originalArrayData
},
//处理数据
processRows(rows) {
const columnValues = [];
for (let col = 0; col < rows[0].length; col++) {
const values = [];
for (let row = 0; row < rows.length; row++) {
values.push(rows[row][col]);
}
// 将每列的值拆分为三份
const partitionSize = Math.ceil(values.length / 3);
const partitions = [];
for (let i = 0; i < values.length; i += partitionSize) {
partitions.push(values.slice(i, i + partitionSize));
}
// 将每份的值拼接成一个字符串,并转换为10进制数
for (let i = 0; i < partitions.length; i++) {
const binaryString = partitions[i].join('');
const decimalValue = parseInt(binaryString, 2);
columnValues.push(decimalValue);
}
}
//计算 n1,n2
const n2 = Math.floor((this.grayscaleArray[0]).length / 256) < 1 ? 0 : Math.floor((this.grayscaleArray[0])
.length / 256); // 求除数
const n1 = this.grayscaleArray[0].length % 256; // 求余数
console.log(n2, n1);
columnValues.unshift(27, 42, 33, n1, n2)
columnValues.push(10)
console.log(columnValues);
return columnValues
},
// 根据阈值将灰度值转换为黑白像素值
convertToBlackWhite(grayscaleData) {
let blackWhiteData = [];
for (let i = 0; i < grayscaleData.length; i += 4) {
let pixel = grayscaleData[i];
let bwPixel = pixel > 128 ? 0 : 1;
blackWhiteData.push(bwPixel);
}
return blackWhiteData;
},
}
}
</script>
<script lang="renderjs" module="canvasImage">
import html2canvas from 'html2canvas'
export default {
data() {
return {}
},
methods: {
// 生成图片需要调用的方法
generateImage(e, ownerVm) {
setTimeout(() => {
const dom = document.getElementById('pagePoster') // 需要生成图片内容的 dom 节点
console.log(dom.clientWidth, dom.clientHeight);
html2canvas(dom, {
width: dom.clientWidth, //dom 原始宽度
height: dom.clientHeight,
scrollY: 0, // html2canvas默认绘制视图内的页面,需要把scrollY,scrollX设置为0
scrollX: 0,
useCORS: true //支持跨域
// , // 设置生成图片的像素比例,默认是1,如果生成的图片模糊的话可以开启该配置项
}).then((canvas) => {
// 创建新的 Canvas
var newCanvas = document.createElement("canvas");
var newContext = newCanvas.getContext("2d");
// 设置新 Canvas 的宽度和高度
var width = 350; // 设置新 Canvas 的宽度
var height = 176; // 设置新 Canvas 的高度
newCanvas.width = width;
newCanvas.height = height;
console.log(newCanvas.height);
// 将 HTML2Canvas 生成的内容绘制到新 Canvas 中
newContext.drawImage(canvas, 0, 0, width, height);
// 将新 Canvas 转换为 base64 图像
var base64 = newCanvas.toDataURL("image/png");
console.log(base64);
// 发送数据到 逻辑层
ownerVm.callMethod('receiveSendData', base64)
}).catch(err => {
})
}, 300)
},
}
}
</script>
<style scoped lang="scss">
.BigBox {
width: 630rpx;
height: auto;
background: #FFFFFF;
border-radius: 20rpx 20rpx 20rpx 20rpx;
display: flex;
justify-content: center;
align-items: center;
}
.barImg {
width: 566rpx;
height: 300rpx;
}
.BigBox2 {
width: 630rpx;
height: auto;
background: #FFFFFF;
border-radius: 20rpx 20rpx 20rpx 20rpx;
display: flex;
justify-content: space-between;
padding: 32rpx 25rpx;
box-sizing: border-box;
margin: 0 auto;
.lf {
width: 362rpx;
view {
width: 100%;
white-space: nowrap;
/* 禁止换行 */
overflow: hidden;
/* 溢出内容隐藏 */
text-overflow: ellipsis;
/* 显示省略号 */
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 40rpx;
color: #000000;
margin-top: 15rpx;
font-weight: 700;
}
view:first-child {
margin-top: 0;
}
}
.rg {
display: flex;
justify-content: center;
align-items: center;
image {
width: 204rpx;
height: 204rpx;
}
}
}
.qrImg {}
.print,
.close {
width: 48%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 10rpx 10rpx 10rpx 10rpx;
border: 2rpx solid #FFFFFF;
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 30rpx;
color: #FFFFFF;
margin-top: 160rpx;
}
.btnBox {
width: 630rpx;
display: flex;
justify-content: space-between;
margin: 0 auto;
}
#myCanvas {
width: 350px;
height: 176px;
opacity: 0;
}
</style>