diff --git a/tools/Mini_Program/iap/README.md b/tools/Mini_Program/iap/README.md new file mode 100644 index 000000000..dd8d33788 --- /dev/null +++ b/tools/Mini_Program/iap/README.md @@ -0,0 +1,2 @@ +## 蓝牙升级微信小程序源码 + diff --git a/tools/Mini_Program/iap/miniprogram/app.js b/tools/Mini_Program/iap/miniprogram/app.js new file mode 100644 index 000000000..9c5d25e2b --- /dev/null +++ b/tools/Mini_Program/iap/miniprogram/app.js @@ -0,0 +1,12 @@ +//app.js +App({ + globalData: { + }, + onLaunch: function () { + this.globalData.SystemInfo = wx.getSystemInfoSync() + //console.log(this.globalData.SystemInfo) + wx.setKeepScreenOn({ + keepScreenOn: true + }) + }, +}) \ No newline at end of file diff --git a/tools/Mini_Program/iap/miniprogram/app.json b/tools/Mini_Program/iap/miniprogram/app.json new file mode 100644 index 000000000..6d45376b0 --- /dev/null +++ b/tools/Mini_Program/iap/miniprogram/app.json @@ -0,0 +1,14 @@ +{ + "pages": [ + "pages/hardware/ble/index/index", + "pages/hardware/ble/device/index" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#f8f8f8", + "navigationBarTitleText": "TencentOS tiny 蓝牙OTA示例", + "navigationBarTextStyle": "black", + "backgroundColor": "#f8f8f8" + }, + "sitemapLocation": "sitemap.json" +} \ No newline at end of file diff --git a/tools/Mini_Program/iap/miniprogram/app.wxss b/tools/Mini_Program/iap/miniprogram/app.wxss new file mode 100644 index 000000000..7dfbc1bc6 --- /dev/null +++ b/tools/Mini_Program/iap/miniprogram/app.wxss @@ -0,0 +1,51 @@ +/**app.wxss**/ +/* .container { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 100rpx 0; + box-sizing: border-box; +} */ + + +.flex-row { + display: flex; + flex-flow: row nowrap; +} + +.flex-row-wrap { + display: flex; + flex-flow: row wrap; +} + +.flex-column-reverse { + display: flex; + flex-flow: column-reverse nowrap; +} + +.center { + justify-content: center; + align-items: center; +} + +.left { + justify-content: flex-start; + align-items: center; +} + +.right { + justify-content: flex-end; + align-items: center; +} + +.space-between { + justify-content: space-between; + align-items: center; +} + +.space-around { + justify-content: space-around; + align-items: center; +} \ No newline at end of file diff --git a/tools/Mini_Program/iap/miniprogram/images/aithinker-mini.png b/tools/Mini_Program/iap/miniprogram/images/aithinker-mini.png new file mode 100644 index 000000000..87743378c Binary files /dev/null and b/tools/Mini_Program/iap/miniprogram/images/aithinker-mini.png differ diff --git a/tools/Mini_Program/iap/miniprogram/images/bluetooth.png b/tools/Mini_Program/iap/miniprogram/images/bluetooth.png new file mode 100644 index 000000000..fdfe30c03 Binary files /dev/null and b/tools/Mini_Program/iap/miniprogram/images/bluetooth.png differ diff --git a/tools/Mini_Program/iap/miniprogram/pages/hardware/ble/device/index.js b/tools/Mini_Program/iap/miniprogram/pages/hardware/ble/device/index.js new file mode 100644 index 000000000..1af9f776a --- /dev/null +++ b/tools/Mini_Program/iap/miniprogram/pages/hardware/ble/device/index.js @@ -0,0 +1,441 @@ +// 1. 小程序发布版本或者体验版本,连续写ble需要加延时,否则会导致写失败1008。本地调试本身有延时,因此不需要加,需要注释掉 +// 2. 安卓系统调试蓝牙会有黏包问题。[ACK, ACK, ..] [C, C] ... +// 3. ymodem.h DOWNLOAD_TIMEOUT 配置需要注意,过小的话,写block时间过长会导致接收端超时,重新进入普通模式 +// 4. NAK 重传逻辑暂不支持 +// https://www.amobbs.com/thread-5704281-1-1.html + +const app = getApp() +const util = require('../../../../utils/util.js'); +const bleapi = require('../../../../utils/hardware/ble/ble-api.js'); +const Packet = require('../../../../utils/hardware/ble/packet.js') + +var msgQueue = [] +var fileBuffer +var fileName +var bUse1K = true +var DownLoadMode = 0 +const SyncTimeout = 15000 // 等待C +const RecvTimeout = 5000 // 等待ACK + +/* +SOH 0x01 协议头(128bytes类型) +STX 0x02 协议头(1k类型) +EOT 0x04 传输结束 +ACK 0x06 接收响应 +NAK 0x15 失败响应 +CAN 0x18 取消传输 +C 0x43 开启文件传输 +*/ +const EOT = 0x04 +const ACK = 0x06 +const CAN = 0x18 +const NAK = 0x15 +const C = 0x43 + +Page({ + data: { + device: { + // connected: true, + // deviceId: "90:9A:77:26:5D:64", + // name: "HC-08", + }, + receiveText: '', + downloadLog: '', + }, + acceptDataFromPrevPage() { + const eventChannel = this.getOpenerEventChannel() + return new Promise((resolve, reject) => { + eventChannel.on('eventData', function (data) { + resolve(data) + }) + }) + }, + onLoad: async function(options) { + console.log(util.formatTime()) + let data = await this.acceptDataFromPrevPage() + console.log("accept data from prev page", data) + this.setData({ + device: data.device, + }) + + // 监听低功耗蓝牙连接状态的改变事件。包括开发者主动连接或断开连接,设备丢失,连接异常断开等等 + await bleapi.onBLEConnectionStateChange(this.onStateChange) + // 监听低功耗蓝牙设备的特征值变化事件。必须先启用 notifyBLECharacteristicValueChange 接口才能接收到设备推送的 notification + if (this.data.device.characteristic.properties.notify == true) { + console.log("notifyBLECharacteristicValueChange and register value change callback") + await bleapi.notifyBLECharacteristicValueChanged( + this.data.device.deviceId, + this.data.device.service.uuid, + this.data.device.characteristic.uuid) + await bleapi.onBLECharacteristicValueChange(this.onMessage) + } + }, + onUnload: function(e) { + let deviceId = this.data.device.deviceId + console.log("onUnload close the ble connection", deviceId) + bleapi.closeBLEConnection(deviceId) + }, + onStateChange: function(res) { + console.log("onBLEConnectionStateChange", res.connected) + this.setData({ + [`device.connected`]: res.connected + }) + }, + formSubmit: async function(e) { + console.log(e.detail.value) + let value = e.detail.value.textarea + try { + await this.writeData(util.str2abUint8(value)) + wx.showToast({ + icon: 'none', + title: '写数据成功', + duration: 1000, + }) + } catch (e) { + wx.showToast({ + icon: 'none', + title: '写数据失败', + duration: 2000, + }) + } + }, + formReset(e) { + console.log(e) + this.setData({ + inputText: '', + }) + }, + bindTextAreaBlur: function(e) { + console.log(e.detail.value) + this.setData({ + inputText: e.detail.value, + }) + }, + chose: function() { + wx.chooseMessageFile({ + count: 1, + type: 'file', + success: (res) => { + console.log(res) + fileName = res.tempFiles[0].name + wx.getFileSystemManager().readFile({ + filePath: res.tempFiles[0].path, + // encoding: 'binary', + success: (res2) => { + console.log(res2) + fileBuffer = new Uint8Array(res2.data) + this.setData({ + file_name: fileName, + file_size: res.tempFiles[0].size, + }) + } + }) + } + }) + }, + writeData: async function(payload) { + console.log(`writeData len(${payload.byteLength})`, payload, new Date().toISOString()) + if (!this.data.device.connected) { + wx.showModal({ + title: '提示', + content: '蓝牙已断开,请重新连接设备', + showCancel: false, + success: function(res) {} + }) + return Promise.reject(`ble disconnect`) + } + + let pos = 0; + let bytes = payload.byteLength + while (bytes > 0) { + let frameBuffer; + if (bytes > 20) { + frameBuffer = payload.slice(pos, pos + 20); + pos += 20; + bytes -= 20; + } else { + frameBuffer = payload.slice(pos, pos + bytes); + pos += bytes; + bytes -= bytes; + } + // console.log(`frame(${frameBuffer.byteLength})`, frameBuffer) + try { + await bleapi.writeBLECharacteristicValue( + this.data.device.deviceId, + this.data.device.service.uuid, + this.data.device.characteristic.uuid, + frameBuffer + ) + // Android手机,连续调用会存在写失败的可能性,建议加延时 + await util.delayMs(20) + } catch (e) { + return Promise.reject(e) + } + } + return Promise.resolve() + }, + onMessage: function(data) { + let datastr = util.ab2strHex(data) + console.log('onMessage:', datastr, msgQueue, new Date().toISOString()) + let str = util.ArrayBuffer2str(data) + this.setData({ + receiveText: this.data.receiveText + str + }) + + if (DownLoadMode) { // 下载模式 + let arr = new Uint8Array(data) + for (let i = 0; i < arr.length; i++) { + if (arr[i] == CAN) { // 取消传输 + DownLoadMode = false + } + msgQueue.push(arr[i]) + } + this.setData({ + downloadLog: this.data.downloadLog + `${datastr}\r\n` + }) + } else { // 普通模式 + console.log("normal onReceive:", str) + } + }, + upload: async function() { + let that = this + if (!this.data.device.connected) { + wx.showModal({ + title: '提示', + content: '蓝牙已断开', + showCancel: false, + success: function(res) {} + }) + return + } + + if (!fileName || fileBuffer.length == 0) { + wx.showModal({ + content: "请先选择固件", + showCancel: false + }); + return + } + + wx.showLoading({ + title: '准备上传...', + mask: true, + }) + this.setData({ + send_percent: 0, + timestamp_start: "", + timestamp_end: "", + }) + DownLoadMode = 1 + console.log(`upload begin. bytes: ${fileBuffer.length} /1024: ${fileBuffer.length/1024}`) + try { + await this.recieveByte(C, SyncTimeout) + + wx.showLoading({ + title: '开始上传...', + mask: true, + }) + + this.setData({ + timestamp_start: util.formatTime(), + date_start: new Date() + }) + + console.log("send blockZero begin") + let id = 0 + let blockZero = Packet.getNormalPacket(id, Packet.getZeroContent(fileName, fileBuffer.length)) + await that.writeData(blockZero.buffer) + await that.recieveByte(ACK, RecvTimeout) + await that.recieveByte(C, SyncTimeout) + console.log("send blockZero complete") + wx.showLoading({ + title: '上传中...', + mask: true, + }) + + console.log("send file begin") + let nInterval = (bUse1K == true) ? 1024 : 128; + for (let i = 0; i < fileBuffer.length; i += nInterval) { + console.log("send block " + (i / nInterval + 1) + " start"); + let upper = (fileBuffer.length < i + nInterval) ? + fileBuffer.length : i + nInterval; + let payloadBuf = new Uint8Array(nInterval); + for (let j = i; j < upper; j++) { + payloadBuf[j - i] = fileBuffer[j]; + } + id = i / nInterval + 1; + let block = (bUse1K == true) ? Packet.getLongPacket(id, payloadBuf) : Packet.getNormalPacket(id, payloadBuf); + + await that.writeData(block.buffer) + try { + // TODO: NAK 重传逻辑 + await that.recieveByte(ACK, RecvTimeout) + } catch (e) { + console.error(`${(i / nInterval) + 1} block lost a ack`, e) + } + + console.log("send block " + ((i / nInterval) + 1) + " succceed!"); + this.setData({ + send_percent: parseInt((i) / fileBuffer.length * 100), + }) + } + console.log("send file complete") + + // EOT处理 + // 超时, + // 返回 NAK, 则重传EOT + // 返回 ACK, 则结束 + console.log("send EOT"); + let sendEOTAndRetry = this.autoRetry(async function(buf) { + await that.writeData(new Uint8Array([EOT]).buffer) + console.log(`waiting ACK`, new Date().toISOString()) + let c = await that.recieve(RecvTimeout) + if (c == ACK) { + console.log(`receive ACK 0x${c.toString(16)}`) + // 处理黏包的情况 [ACK, ACK] + while (msgQueue.length > 0) { + if (msgQueue[0] == ACK) { + let c = msgQueue.shift() + console.log(`shift ${c.toString(16)} from queue`, msgQueue) + } else { + break + } + } + } else if (c == NAK) { + console.log(`receive NAK 0x${c.toString(16)}`) + // 处理黏包情况 [NAK, ACK, C] + if (msgQueue.length > 0) { + await that.recieveByte(ACK, RecvTimeout) + } else { + throw `receive NAK 0x${c.toString(16)}, resend EOT` + } + } + }, 2) + await sendEOTAndRetry(new Uint8Array([EOT]).buffer) + await that.recieveByte(C, SyncTimeout) + + console.log("send last block begin") + let blockLast = Packet.getNormalPacket(0, new Uint8Array(128)); + await that.writeData(blockLast.buffer) + await that.recieveByte(ACK, RecvTimeout) + console.log("send last block compelte") + + DownLoadMode = 0 + let diff = new Date().getTime() - this.data.date_start.getTime() + let rate = Math.round(fileBuffer.length * 1000 / diff) * 100 / 100 + console.log("upload complete") + this.setData({ + send_percent: 100, + timestamp_end: util.formatTime(), + rate: rate, + }) + wx.showModal({ + content: "上传完成", + showCancel: false + }); + } catch (e) { + DownLoadMode = 0 + console.error("upload fail:", e) + wx.showModal({ + content: "上传失败,请重试\r\n" + e, + showCancel: false + }); + } + wx.hideLoading() + console.log("msgQueue", msgQueue) + msgQueue = [] // clear msgQueue + }, + reciveByteAndRetry: async function(expect, timeout, retryNum) { + let func = this.autoRetry(this.recieveByte, retryNum) + await func(expect, timeout) + }, + recieveByte: async function(expect, timeout) { + console.log(`waiting 0x${expect.toString(16)} for ${timeout}ms ...`, new Date().toISOString()) + try { + let c = await this.recieve(timeout) + if (c == CAN) { // 接收端终止 + throw `receive 0x${c.toString(16)}. cancel tranmission` + } + if (c != expect) { + throw `expect 0x${expect.toString(16)}, but recieve 0x${c.toString(16)}` + } + // 处理黏包的情况 + while (msgQueue.length > 0) { + if (msgQueue[0] == expect) { + let c = msgQueue.shift() + console.log(`shift ${c.toString(16)} from queue`, msgQueue) + } else { + break + } + } + Promise.resolve(c) + } catch (e) { + throw `waiting 0x${expect.toString(16)} fail: ${e}` + } + }, + recieve: async function(timeout) { + // console.log("start recieve", new Date().toISOString()) + let elaspe = timeout + const interval = 500 + while (elaspe > 0) { + elaspe = elaspe - interval + // console.log(`xxxx`, msgQueue.length, elaspe) + if (msgQueue.length > 0) { + let c = msgQueue.shift(); + console.log(`receive from queue: ${c.toString(16)}`, msgQueue) + return Promise.resolve(c) + } + await util.delayMs(interval) + } + return Promise.reject(`receive timeout ${timeout}ms ${new Date().toISOString()}`) + }, + /* + example: + let sendLastblockAndRetry = this.autoRetry(async function (buf) { + await that.writeData(buf); + await that.recieveByte(ACK, RecvTimeout) + }, 3) + await sendLastblockAndRetry(blockLast.buffer) + */ + autoRetry(func, retryMax) { + let retryNum = retryMax; + return async function funcR() { + let params = arguments + while (retryNum--) { + try { + await func(...params) + break + } catch (e) { + if (retryNum > 0) { + console.error(`retry ${retryMax - retryNum} time. error:`, e); + continue + } else { + throw e + } + } + } + } + }, + RecvCleanTap: function() { + this.setData({ + receiveText: '' + }) + }, + sendValue: async function(e) { + let val = e.currentTarget.dataset.val + try { + await this.writeData(util.str2abUint8(val)) + wx.showToast({ + icon: 'none', + title: '发送成功', + duration: 1000, + }) + } catch (e) { + wx.showToast({ + icon: 'none', + title: '发送失败', + duration: 2000, + }) + } + }, +}) \ No newline at end of file diff --git a/tools/Mini_Program/iap/miniprogram/pages/hardware/ble/device/index.json b/tools/Mini_Program/iap/miniprogram/pages/hardware/ble/device/index.json new file mode 100644 index 000000000..8835af069 --- /dev/null +++ b/tools/Mini_Program/iap/miniprogram/pages/hardware/ble/device/index.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/tools/Mini_Program/iap/miniprogram/pages/hardware/ble/device/index.wxml b/tools/Mini_Program/iap/miniprogram/pages/hardware/ble/device/index.wxml new file mode 100644 index 000000000..fb70629ef --- /dev/null +++ b/tools/Mini_Program/iap/miniprogram/pages/hardware/ble/device/index.wxml @@ -0,0 +1,94 @@ + + + + + + + 设备信息 + + + + 设备名称 + {{device.name}} + + + 设备ID + {{device.deviceId}} + + + 连接状态 + +