uniapp+vue3开发H5或APP蓝牙工具,处理下载文件、解析、加密、写入进行OTA固件升级
·
本次下载文件仅处理线上文件,没有本地文件的需求,就没去研究,估计大差不差
第一步:下载并分割文件。
以下是下载并分割文件内容的核心方法useFileProcessorEncryption.js
processFile(fileUrl, chunkSize = 128)主函数如果是H5或者是需要监听文件下载进度则有这个方法downloadFile(fileUrl)如果是H5就用downloadFile(fileUrl)拿到的临时文件路径processDownloadedFile(downloadResult.tempFilePath, chunkSize);如果是APP就用真实的线上文件URL链接 processDownloadedFile(fileUrl, chunkSize)readFileAsArrayBuffer(tempFilePath)方法,读取文件为ArrayBuffer。经验证,uni.request下载文件在H5和APP全部通用,XMLHttpRequest()和fetch仅能在H5中使用。splitIntoChunks(arrayBuffer, chunkSize)分割文件。 ArrayBuffer是原始二进制数据容器,类似于 "内存区域" 或 "二进制数据仓库",本身不能直接访问数据,需要"视图"来读写数据。 Uint8Array是访问 ArrayBuffer 的 "窗口" 或 "接口",提供了读写 ArrayBuffer 中数据的方法。- fillArray(Math.ceil(totalLength / chunkSize))是我这里为了生存数据帧起始标志用的,[1,2,3,4,5,…,FFFF]。
arrayBufferToBase64(buffer) 可以把buffer数据转为base64,我这里虽然给出了这个方法,但最后只是测试了一下,没有真正取用,是因为打印出来的数据不如十六进制字符串方便查看- AES128加密可以直接加密分割后的ArrayBuffer二进制数据,也可以把二进制数据转为base64或者十六进制,虽然加密之后得到的加密串是不一样的,但是用相应的解密方法,得到的解密数据是一样的
/**
* composables/useFileProcessorEncryption.js
* 下载文件,并读取文件内容进行分割处理
* 在h5中使用XMLHttpRequest或者fetch或者uni.request都可以。
* 在APP端,使用uni.request,APP中没有XMLHttpRequest,没有fetch
*/
import { ref } from 'vue'
export function useFileProcessor() {
const isLoading = ref(false) //是否正在加载中
const progress = ref(0) //进度
const result = ref(null) //处理结果
const error = ref(null) //错误信息
let start_head_list = ref([]) //开始头列表, 十进制,1~65535,最后一个必须是65535,前面的正常+1,例如:[1, 2, 3, 4, 5, 65535]
let fileALLData = ref({
fileALLData_ArrayBuffer: null,
size: 0,
}) //文件下载之后获取到的所有内容,是一个ArrayBuffer
/**
* 下载文件
* @param {*} url
* @returns
*/
const downloadFile = (url) => {
return new Promise((resolve, reject) => {
const downloadTask = uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
// console.log(res)
resolve(res)
} else {
reject(new Error(`下载失败,状态码: ${res.statusCode}`))
}
},
fail: reject,
})
// 监听下载进度
downloadTask.onProgressUpdate((res) => {
progress.value = res.progress
})
})
}
/**
* 读取文件为ArrayBuffer
* @param {*} filePath 临时文件路径或者文件路径
* @returns ArrayBuffer数据
*/
const readFileAsArrayBuffer = (filePath) => {
return new Promise((resolve, reject) => {
// 在h5中使用xhr或者fetch都可以。在APP端,使用uni.request
// #ifdef H5
// const xhr = new XMLHttpRequest()
// xhr.open('GET', filePath, true)
// xhr.responseType = 'arraybuffer'
// xhr.onload = function () {
// if (xhr.status === 200) {
// console.log('文件读取成功', xhr)
// resolve(xhr.response)
// } else {
// reject(new Error('文件读取失败'))
// }
// }
// xhr.onerror = function () {
// reject(new Error('文件读取错误'))
// }
// xhr.send()
// fetch(filePath)
// .then((res) => res.arrayBuffer())
// .then((buffer) => {
// console.log('文件读取成功', buffer)
// // const bytes = new Uint8Array(buffer)
// // resolve(splitBytes(bytes))
// resolve(buffer)
// })
// .catch((err) => reject(err))
// #endif
uni.request({
url: filePath,
method: 'GET',
responseType: 'arraybuffer', // 关键点
success: (res) => {
if (res.statusCode === 200) {
console.log('文件读取成功', res)
fileALLData.value = {
fileALLData_ArrayBuffer: res.data,
size: res.data.byteLength,
}
resolve(res.data)
} else {
reject('下载失败:' + res.statusCode)
}
},
fail: (err) => {
reject('请求出错: ' + JSON.stringify(err))
},
})
})
}
// 更简单的方法:直接使用uni.downloadFile返回的临时文件路径
const processDownloadedFile = async (tempFilePath, chunkSize = 128) => {
try {
// 在APP端,我们可以直接使用文件的ArrayBuffer
// 这里提供一个替代方案:如果需要处理文件内容,可以重新下载或使用其他方式
const arrayBuffer = await readFileAsArrayBuffer(tempFilePath)
return splitIntoChunks(arrayBuffer, chunkSize)
} catch (err) {
console.warn('直接读取文件失败,使用替代方案:', err)
// 如果直接读取失败,可以返回空数据或使用其他处理方式
return [new Uint8Array(chunkSize).fill(0)]
}
}
// 处理每一条指令的头,从1开始最大为65535,也就是FFFF
const fillArray = (length) => {
// 处理长度小于1的情况
if (length < 1) return []
// 处理长度为1的情况
if (length === 1) return [65535]
// 常规情况处理
const arr = []
for (let i = 1; i < length; i++) {
arr.push(i)
}
arr.push(65535)
return arr
}
// 分割文件内容
const splitIntoChunks = (arrayBuffer, chunkSize = 128) => {
const chunks = []
const uint8Array = new Uint8Array(arrayBuffer)
const totalLength = uint8Array.length
start_head_list.value = fillArray(Math.ceil(totalLength / chunkSize))
console.log('start_head_list', start_head_list.value)
for (let i = 0; i < totalLength; i += chunkSize) {
const end = Math.min(i + chunkSize, totalLength)
const chunk = uint8Array.slice(i, end)
// 如果块不足128字节,补0
if (chunk.length < chunkSize) {
const paddedChunk = new Uint8Array(chunkSize)
paddedChunk.set(chunk)
paddedChunk.fill(0, chunk.length)
chunks.push(paddedChunk)
} else {
chunks.push(chunk)
}
}
// 处理空文件的情况
if (chunks.length === 0) {
chunks.push(new Uint8Array(chunkSize).fill(0))
}
return chunks
}
// ArrayBuffer 转 Base64
const arrayBufferToBase64 = (buffer) => {
const binary = []
const bytes = new Uint8Array(buffer)
for (let i = 0; i < bytes.byteLength; i++) {
binary.push(String.fromCharCode(bytes[i]))
}
return binary.join('')
// return window.btoa(binary.join(''))
}
/**
* 下载并处理文件
* @param {*} fileUrl 文件URL
* @param {*} chunkSize 分块大小
* @returns
*/
const processFile = async (fileUrl, chunkSize = 128) => {
isLoading.value = true
error.value = null
progress.value = 0
try {
// 1. 下载文件
progress.value = 30
const downloadResult = await downloadFile(fileUrl)
console.log(downloadResult)
if (downloadResult.statusCode !== 200) {
throw new Error(`下载失败,状态码: ${downloadResult.statusCode}`)
}
// 2. 处理下载的文件
progress.value = 60
let chunks = []
// #ifdef H5
chunks = await processDownloadedFile(downloadResult.tempFilePath, chunkSize)
// #endif
// #ifdef APP-PLUS
chunks = await processDownloadedFile(fileUrl, chunkSize)
// #endif
// 3. 准备结果
progress.value = 90
result.value = {
chunks, // 处理后的所有的块
totalChunks: chunks.length, // 总块数
tempFilePath: downloadResult.tempFilePath, // 下载的临时文件路径
chunksBase64: chunks.map((chunk) => arrayBufferToBase64(chunk)), // 每个块的Base64编码
}
progress.value = 100
return result.value
} catch (err) {
error.value = err.message || '处理文件时发生错误'
console.error('文件处理错误:', err)
throw err
} finally {
isLoading.value = false
}
}
/**
* 重置
*/
const reset = () => {
isLoading.value = false
progress.value = 0
result.value = null
error.value = null
}
return {
isLoading,
progress,
result,
error,
start_head_list,
processFile,
reset,
arrayBufferToBase64,
fileALLData,
}
}
以下是在自定义的一个底部弹窗面板的文件中使用的。
第二步:拿到下载文件的所有数据的Uint8Array(已分割的)。可以直接去加密这些原始数据。
/**
* 第一步:点击下载并处理文件,进行OTA配置
*/
const handleProcessFile = async () => {
if (progressTransfer.value == 100 || isLoadingTransfer.value) return
if (!fileUrl.value) {
uni.showToast({
title: '请输入文件URL',
icon: 'none',
})
return
}
try {
// 第二步:调用下载并分割文件的核心函数
const result = await processFile(fileUrl.value, 128)
console.log('处理结果:', result)
dataListALL_Uint8Array = result.chunks //存储所有数据的Uint8Array
result.chunks.map((item) => {
let temp = Array.from(item).map((num) => num.toString(16).toUpperCase().padStart(2, '0'))
dataListALL_HexString.push(temp.join('')) //把Uint8Array转换成十六进制字符串
})
// console.log('所有数据的Uint8Array', dataListALL_Uint8Array)
// console.log('所有数据的十六进制字符串', dataListALL_HexString)
// 开始下发数据
transferCount.value = 0
progressTransfer.value = 0
isLoadingTransfer.value = true
logsTransfer.value.length = 0
// 第三步:开始执行所有命令
executeAllCommands()
} catch (err) {
console.error('处理失败:', err)
uni.showToast({
title: '处理失败',
icon: 'error',
})
}
}
第三步:拿到下载文件的所有数据的Uint8Array(已分割的)。可以把每个Uint8Array转为十六进制字符串,加密十六进制字符串
由于对原始数据根据128字节每段做了分割,不足的也进行补0处理,保证每段数据都是128字节的,所以无论是加密原始数据Uint8Array还是加密十六进制字符串,加密之后的数据都是128字节的
/****************以下是我的要写入到蓝牙的指令的格式。****************/
/* 先下发一个起始帧,起始头是0,*/
typedef struct
{
uint16_t start_head; // 0x0000 (起始标志)
/******** 128B ***********/
uint8_t block_size; // 128=1 (块大小)
uint8_t device_type[2]; // 设备类型(0x0046)
uint8_t ack; // 是否应答(noack=0)
uint8_t secret_key_type; // 加密方式(AES128=1)
uint8_t secret_key[16]; // 密钥 (原始:0x00000000000000000102030405060708)
uint8_t firmware_version[4]; // 固件版本号(0x01010101)
uint8_t firmware_size[8]; // 固件大小 (字节)
uint8_t ver_type; // 校验方式 (SHA256=1)
uint8_t ver_data[32]; // 这里是使用SHA256对所有的原始二进制数据(未分割的)进行校验之后的串
uint8_t reserve; // 目前取值0
uint8_t url_type; // 目前取值0
uint8_t url_data[56]; // 目前全部置为0
reserve[4];
/**********************/
uint32_t crc_32; // CRC32校验 (128B数据区)
} start_frame;
//----------------------------------------------------------
typedef struct
{
uint16_t start_head; // 0x0000 (起始标志)
/******** 128B ***********/
uint8_t block_size; // 64=0、128=1、1024=2 (块大小)
uint8_t device_type[2]; // 设备类型(0x0046)
uint8_t ack; // 是否应答 (noack=0/ack=1)
uint8_t secret_key_type; // 加密方式(DES=0/AES128=1)
uint8_t secret_key[16]; // 密钥 (DES/AES128)
uint8_t firmware_version[4]; // 固件版本号
uint8_t firmware_size[8]; // 固件大小 (字节)
uint8_t ver_type; // 校验方式 (CRC32=0/SHA256=1)
uint8_t ver_data[32]; // 校验数据 (CRC32=0/SHA256=1)
uint8_t reserve; // 0
uint8_t url_type; // 路径类型(ID=0/URL=1)
uint8_t url_data[56]; // 选中升级ID(低32byte,0xFF表示所有设备\固件下载URL(UTF-8))不足补0
reserve[4];
/**********************/
uint32_t crc_32; // CRC32校验 (128B数据区)
} start_frame;
/* 以下开始下发数据帧,起始头从1开始,最大到65535 */
typedef struct
{
uint16_t frame_num, //1-65534
/********128B***********/
uint8_t data[128], //有效数据区
/**********************/
uint32_t crc_32, //crc32
}data_frame;
/* 以下是下发结束帧,起始头为65535 */
typedef struct
{
uint16_t end_head, //ffff
/********128B***********/
uint8_t data[128], //不足补0
/**********************/
uint32_t crc_32, //crc32
}end_frame;
/*
数据帧和结束帧中的数据包128Bytes都是对下载的文件分割并加密过后的
*/
第四步:下发起始帧
/**
* 生成SHA256 哈希值
* @param {*} data
*/
const generateSHA256 = (data) => {
const hexHash = SHA256(data).toString(encHex)
console.log(hexHash)
return hexHash
}
/**
* 下发起始帧
* @param {*} ALLData 下载文件拿到的所有数据
*/
const createStart_frame = () => {
let start_head = '0000' //起始标志
let block_size = '01' // 数据块大小(64Bytes为0,128Bytes为1,1024Bytes为2) --1字节
let device_type = '0046' // 设备类型 --2字节
let ack = '00' //应答标志(no ack=0,ack=1) --1字节
let secret_key_type = '01' //加密方式(DES=0,AES128=1) --1字节
let secret_key = '00000000000000000102030405060708' //密钥 --16字节
let firmware_version = '01010101' //固件版本号 --4字节
let firmware_size = '0000000000000010' //固件大小(AES128为16) --8字节
let ver_type = '01' //校验方式(CRC32=0,SHA256=1) --1字节
let ver_data = generateSHA256(fileALLData.value.fileALLData_ArrayBuffer) //校验数据(下载文件的所有数据) --32字节
let reserve = '00' //保留(0) --1字节
let url_type = '00' //路径类型(ID=0,URL=1) --1字节
let url_data = toHexChars(0, 112) // 升级ID或者下载的URL --56字节
let reserve4 = '00000000' //保留(0) --4字节
// 以上加起来共128字节
let data =
block_size +
device_type +
ack +
secret_key_type +
secret_key +
firmware_version +
firmware_size +
ver_type +
ver_data +
reserve +
url_type +
url_data +
reserve4
let data_crc32 = crc32(data)
let command = start_head + data + data_crc32
// 第四--1步,调用封装的下发指令的公共方法
publicFun_sendCommand(command, 0)
}
/**
* 下发指令的公共方法
* 以下有很多变量和函数是关于蓝牙写入数据的,比如sendPluralMessages是多写'ff10',crc32()是生成校验值的。
* 具体的不在这篇展示了,要不然整乱了,以后在其他文章完善吧,这篇主要描述OTA固件升级的。
* @param {*} command 拼好的十六进制指令串
*/
const publicFun_sendCommand = (command, index) => {
let registerCount = twoToFourArr(strToTwoArr(command)).length.toString(16).padStart(4, '0')
let byteCount = strToTwoArr(command).length.toString(16).padStart(2, '0')
let result =
sendPluralMessages +
toFourHexChars(planConfig.OTAAddr) +
registerCount +
byteCount +
command +
crc32(
sendPluralMessages + toFourHexChars(planConfig.OTAAddr) + registerCount + byteCount + command,
)
console.log(command, result)
let matchPlanList = {
tag: 'OTA配置',
addr: planConfig.OTAAddr,
planName: 'OTA配置',
}
// 更新进度
progressTransfer.value = parseFloat(
(((index + 1) / dataListALL_HexString.length) * 100).toFixed(1),
)
console.log('进度', progressTransfer.value)
emit('emitSetOTA', {
planName: 'OTA配置',
fromMark: '',
addr: planConfig.OTAAddr,
params: result,
tip: '多写',
matchPlanList: matchPlanList,
})
}
第五步:下发数据帧,分段下发,128字节是一段,最后一段是结束帧(通过其实标志头FFFF判断)
/**
* 拼接数据串,下发指令
* @param {*} list 数据串
* @param {*} index
*/
const createData_frame = async (list, index) => {
let start_head = toFourHexChars(start_head_list.value[index])
let data = list
let data_crc32 = crc32(data)
let command = start_head + data + data_crc32
console.log(twoToFourArr(strToTwoArr(command)))
// 第五--1步,调用封装的下发指令的公共方法
publicFun_sendCommand(command, index)
}
/**
* AES128加密
* 密钥随便写的,按约定来,只要是十六字节的十六进制串就行
* @param {*} rawData 原始数据:二进制数据或者十六进制字符串
*/
const AES128_Eecrypt = (rawData, type = 'Hex') => {
console.log('原始数据:', rawData)
let resultHex = ''
if (type == 'Hex') {
// console.log('=== 验证加密大小 ===')
// console.log('输入十六进制字符数:', rawData.length)
// console.log('输入数据字节数:', rawData.length / 2)
// 将十六进制字符串转换为二进制WordArray
const data = encHex.parse(rawData)
const key = encHex.parse(encryptionKey)
const encrypted_Hex = CryptoJS.AES.encrypt(data, key, {
mode: CryptoJS.mode.ECB, //(如果使用CBC需要提供iv)
// padding: CryptoJS.pad.Pkcs7,
padding: CryptoJS.pad.NoPadding,
})
// 将加密后的二进制数据转换为十六进制字符串格式
resultHex = encrypted_Hex.ciphertext.toString(encHex)
console.log('Hex加密结果:', resultHex)
// console.log('输出十六进制字符数:', resultHex.length)
// console.log('输出数据字节数:', resultHex.length / 2)
// console.log('大小是否相同:', rawData.length === resultHex.length)
} else if (type == 'Uint8Array') {
const wordArray = CryptoJS.lib.WordArray.create(rawData) //二进制数据转换为WordArray
const keyWordArray = encHex.parse(encryptionKey)
const encrypted_Uint8Array = CryptoJS.AES.encrypt(wordArray, keyWordArray, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding,
})
resultHex = encrypted_Uint8Array.ciphertext.toString(encHex)
console.log('Uint8Array加密结果:', resultHex)
}
AES128_Decrypt(resultHex) //测试解密
return resultHex
}
/*
* 下发数据帧
* @param {*} data 加密之后的数据串, 十六进制的
* @param {*} index 传输的次数,用来获取起始标志头,判断什么时候结束传输
*/
createData_frame(
AES128_Eecrypt(dataListALL_Uint8Array[transferCount.value], 'Uint8Array'),
transferCount.value,
) //加密的是Uint8Array。
/**
* AES128解密,用来自己测试加密的数据是否正确
* @param {*} encryptedData 加密的数据:二进制数据或者十六进制字符串
*/
const AES128_Decrypt = (encryptedData) => {
const key = encHex.parse(encryptionKey)
//创建wordArray
const ciphertext = encHex.parse(encryptedData)
//创建CipherParams对象
const cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: ciphertext,
})
let plaintext = CryptoJS.AES.decrypt(cipherParams, key, {
mode: CryptoJS.mode.ECB,
// padding: CryptoJS.pad.Pkcs7,
padding: CryptoJS.pad.NoPadding,
}).toString(encHex)
console.log('解密结果:', plaintext)
console.log('=== 解密完成 ===')
}
更多推荐

所有评论(0)