序: 小程序需要将图片上传到蓝牙设备,那么图片要转为二进制数据,转为二进制的图片数据不能过大,就要裁剪图片,因为要尽量减少图片的二进制数据,要将图片的rgb888转为rgb565,也就是24位缩小为16位数据;例如:100像素*100像素的图片,就有1万个点,一个点包含rgba四个数,只需要rgb这三个数,rgb三个数各包含8位(也就是二进制,为rgb888),rgb888为三个字节,rgb565为两个字节,数据量传输就少了三分一,但也有几十K的数据了,分240个字节每包发送完也要十秒左右;
如果要上传,上传到后端的路径要是临时路径才可以,不能是本地路径,所以要使用 canvas 转一下得到 tempFilePath 的临时路径,
一、图片渲染为 canvas 得到临时路径,并上传
// 选择本地图片渲染成 canvas
locationImg(e) {
let imgUrl = e.currentTarget.dataset.img;
let that = this;
gbImgComStr = "";
wx.createSelectorQuery().select('#canvasImg').fields({
node: true,
size: true
}).exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
var dpr = wx.getSystemInfoSync().pixelRatio // 设备像素比
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
const img = canvas.createImage();
img.onload = () => {
ctx.drawImage(img, 0, 0, 200, 200);
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: 200,
height: 200,
destWidth: 200,
destHeight: 200,
canvas: canvas,
fileType: "jpg",
quality: .8,
success(res) {
that.initImgData(res.tempFilePath); // 使用服务器
}
})
}
img.src = imgUrl;
})
// console.log(imgUrl)
},
// 图片上传到服务器
initImgData(imgUrl) {
let that = this;
let gbImgPath = "";
wx.uploadFile({
url: apiUrl+'/imgUpload', //你的后端上传路径
filePath: imgUrl,
name: "imgPath", // 键值key
header: { "content-type": "multipart/form-data"},
success: function (res) {
if (res.statusCode == 200) {
gbImgPath = JSON.parse(res.data).imgPath; // 存放到服务器上的图片路径
}
},
fail: function (err) {
wx.showToast({
title: "上传失败",
icon: "none",
duration: 2000
})
},
})
// 图片回显 或 gbImgPath
that.setData({
imgPath: imgUrl
})
},
后端接口(PHP):
//对应的文件夹
public function dirFile($name){
$dirName = "store/".$name."/".date("Ym");
if(!file_exists($dirName)){
mkdir(app()->getRootPath().'public/'.$dirName,0777,true);
}
return $dirName;
}
// 图片处理
function imgUpload(){
if(request()->isPost()){
$dirName = $this -> dirFile('image');
$file = $_FILES['imgPath']; // 获取小程序传来的图片
if(is_uploaded_file($file['tmp_name'])) {
$type = $file["type"];//文件类型 如:image/png
$ext = strrchr($type, '/');//切割字符串
//类型判断
if (in_array($ext, ['/jpeg', '/jpg', '/png', '/svg', '/gif','ico'])) {
$filePrefix1 = strstr($file["name"], '.', true);// 文件名前缀 无.png
$filestr = date("Ymdhis").$filePrefix1; // 避免同一时间调用该方法时产生同一个文件名
$fileExt1 = strstr($file["name"], '.'); // 获取 .png
$fileName = $dirName . "/" . md5($filestr) . $fileExt1; //拼接文件路径
//判断文件是否存在
// if (file_exists($fileName)) {
// return json(["statusCode" => 200,"msg"=>"exist","imgPath" => $fileName]);
// } else {
//保存文件 ($_FILES["文件名"]["tmp_name"],newPath),(旧路径,新路径)
move_uploaded_file($file["tmp_name"], $fileName);
return json(["statusCode" => 200,"msg"=>"success","imgPath" => $fileName]);
// }
}
return json(["statusCode" => 402,"msg"=>"format error"]);
}
return json(["statusCode"=>401,"msg"=>"file error"]);
}
return json(["statusCode"=>400,"msg"=>"request error"]);
}
二、在小程序上将图片数据rgb888转为rgb565
var gbImgComStr = "";
// 图片:16进制补零
function imgHexFill(hexStr) {
let len = hexStr.length;
var map = {
1: "000",
2: "00",
3: "0"
}
if (map[len]) {
hexStr = map[len] + hexStr;
}
return hexStr;
}
// 上为 Page 外,下为 Page 内
// 图片画成 canvas 获取数据
initImgData565: async function(imgUrl) {
return new Promise((resolve, reject) => {
let that = this;
imgLimitNum = 0;
wx.createSelectorQuery().select('#canvasImg').fields({
node: true,
size: true
}).exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
// var dpr = wx.getSystemInfoSync().pixelRatio // 设备像素比
var dpr = 1;
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
// ctx.scale(dpr, dpr)
let sideLen = 100;
ctx.fillRect(0, 0, sideLen, sideLen);
const img = canvas.createImage();
img.onload = () => {
ctx.drawImage(img, 0, 0, sideLen, sideLen);
var imageData = ctx.getImageData(0, 0, sideLen, sideLen).data;
// console.log(imageData.length)
var rgbStr = "";
var rgb = 0;
var rgb565 = 0;
var w = 0; var h = 0;
// 逐列式 (逐行式则是Z扫描)
for (var w = 0; w < sideLen; w++) {
for (var h = 0; h < sideLen; h++) {
var offset = (h*sideLen + w)*4;
var red = imageData[offset];
var green = imageData[offset + 1];
var blue = imageData[offset + 2];
rgb = 0xff000000 | (red << 16) | (green << 8) | blue;
rgb565 = ((rgb & 0xf80000) >> 8) | ((rgb & 0xfc00) >> 5) | ((rgb & 0xf8) >> 3)
rgbStr += imgHexFill(rgb565.toString(16));
}
}
gbImgComStr = rgbStr.toUpperCase();
imgUrl != "" ? resolve() : reject();
}
img.src = imgUrl;
})
})
},
由于数据过大,这些数据还要分包发送到蓝牙设备,首先,要发第一包数据告诉设备要发图片的指令了,可以加入长度,这个要和设备那边沟通,分包发送还不能全部直接发,不然蓝牙设备很容易造成丢包的,小程序发完了,设备那边可能才获取十分之一呢,要分20、30包发完后再发一个确定指令,如果返回正确的指令则继续分20、30包发下去再发一个确定指令,然后重复发完为止;
部分代码:
// 图片_下个数据包指令
imgNextCmd: async function(){
let that = this;
let sData = that.data;
let comStr = "", roleData = "", command = "";
let rlt = "";
let packageNum = 30; // 连续发送包的个数
var rowSum = ((imgPackageSum - imgIndex) >= packageNum) ? (imgIndex + packageNum) : imgPackageSum;
for (let row = imgIndex; row < rowSum; row++) {
comStr = gbImgComStr.substr((row)*(imgCharLen-10),(imgCharLen-10));
roleData = "AC" + decToHexLenFill(row+1) + decToHex(comStr.length/2) + comStr;
let res = new Uint8Array(roleData.match(/[\da-f]{2}/gi).map(function (h) {
return parseInt(h, 16)
}));
command = roleData + arrSumCheck(res);
imgIndex += 1;
// await sleep(50);
rlt = await that.sendCom(command, sData.deviceId, sData.serviceId, sData.characteristicId);
if (rlt == "success") {
// 包确认
if(row == (rowSum-1)){
// console.log(">>> sure",rowSum)// 成功
let sureCom = "BCxxxx"+decToHexLenFill(row+1);
let cs = new Uint8Array(sureCom.match(/[\da-f]{2}/gi).map(function (h) {
return parseInt(h, 16)
}));
let sureCommand = sureCom + arrSumCheck(cs);
that.writeBLECharacteristicValue(sureCommand);
}
}
}
if(rowSum == imgPackageSum && imgLimitNum == 0){
imgLimitNum = 1;
let endCom = "xxx"; // 传输完毕
that.writeBLECharacteristicValue(endCom);
}
},
彩蛋: 一般一个包只能发20个字节,你觉得一张150*150的图片数据发完需要多久?要两分钟左右吧,这个还没考虑中途断连等情况呢!你觉得你是用户,你会接受吗?所以,可以先获取一下mtu是很有必要的,一般Android的是23(包含头部3个字节信息),iOS的是512,有部分Android手机是没有mtu值的,只能落空默认值了,Android可以协商mtu值 wx.setBLEMTU ,你可以一个包发256、300个字节,看你需求,过大会产生一些问题,并且每包发送也可以设置一下延迟多少毫秒,也可以避免传输过程中断连问题。
评论 (0)