前言
這是個藍牙打印圖片的功能,業務是打印界面固定的demo范圍,這里通過html2canvas插件生成的圖片base64,然后圖片base64繪制到canvas中去后,獲取canvas中的像素信息,然后對像素信息進行一個灰度值處理,灰度值處理后再進行黑白值的處理,然后再根據藍牙機需要通過圖片的寬高比例進行一個二維數組的生成等等,一番數據處理后,將數據轉換為buffer格式 ,因為藍牙一次只能發20字節所以采用遞歸方式發送。
采用貼紙打印的話,需要使用黑標指令發送固定數據給藍牙一次,打印機會蜂鳴,目的是打印內容完后,自動切換下一個紙準備,不會仍舊在當前紙打印。
這里用的是南方鴻志科技的58mm熱敏打印機,
總結:打印的view視圖范圍中有文字有圖片,其中圖片是base64格式是正常運行的,如果是本地圖片會發現html2canvas在app端無法處理。
- 搜索連接藍牙界面
- 藍牙打印界面
- 引用組件界面
搜索連接藍牙界面
<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.deviceIdlet DeviceID = device.deviceId //這里是拿到的uuidthis.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 = thisuni.createBLEConnection({ //創建藍牙連接,連接低功耗藍牙設備deviceId: DeviceID, //傳入剛剛獲取的uuidsuccess(res) {console.log("第五步 創建藍牙連接成功:", JSON.stringify(res))doc.GetBLEDeviceServices(DeviceID) //獲取藍牙設備所有服務(service)。},fail(res) {console.log(res)}})},//第六步 獲取藍牙設備所有服務(service)。GetBLEDeviceServices(DeviceID, index) {let doc = thissetTimeout(function() { //這里為什么要用setTimeout呢,等等下面會解釋uni.getBLEDeviceServices({ //獲取藍牙設備所有服務deviceId: DeviceID,success(res) { //為什么要用延時,因為不用延時就拿不到所有的服務,在上一步,連接低功耗藍牙//設備的時候,需要一個600-1000毫秒的時間后,再去獲取設備所有服務,不給延時就會一直返回錯誤碼10004console.log("第六步 獲取藍牙設備所有服務:", JSON.stringify(res))uni.setStorageSync("ServiceUUID", res.services[2].uuid) //把已經連接的藍牙設備信息放入緩存uni.setStorageSync("ServiceUUIDNew", res.services[2].uuid) //把已經連接的藍牙設備信息放入緩存let ServiceUUIDNew = res.services[2].uuidthis.ServiceUUID = res.services[2].uuidconsole.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-IOSuni.setStorageSync("CharacteristicId", res.characteristics[0].uuid) //把某個服務中所有特征值信息放入緩存that.characteristicId = res.characteristics[0].uuidconsole.log(res);// #endif// #ifdef APP-ANDROIDuni.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>
藍牙打印界面
<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>
引用組件界面
<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;// 將灰度值賦給 RGBimageData[i] = grayscale; // RedimageData[i + 1] = grayscale; // GreenimageData[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 屬性,即得到對應的 ArrayBufferconst 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 = originalArraylet replenish = 24 - height % 24for (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,n2const 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設置為0scrollX: 0,useCORS: true //支持跨域// , // 設置生成圖片的像素比例,默認是1,如果生成的圖片模糊的話可以開啟該配置項}).then((canvas) => {// 創建新的 Canvasvar 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>