本次下载文件仅处理线上文件,没有本地文件的需求,就没去研究,估计大差不差

第一步:下载并分割文件。


以下是下载并分割文件内容的核心方法useFileProcessorEncryption.js

  1. processFile(fileUrl, chunkSize = 128)主函数
  2. 如果是H5或者是需要监听文件下载进度则有这个方法downloadFile(fileUrl)
  3. 如果是H5就用downloadFile(fileUrl)拿到的临时文件路径processDownloadedFile(downloadResult.tempFilePath, chunkSize);如果是APP就用真实的线上文件URL链接 processDownloadedFile(fileUrl, chunkSize)
  4. readFileAsArrayBuffer(tempFilePath)方法,读取文件为ArrayBuffer。经验证,uni.request下载文件在H5和APP全部通用,XMLHttpRequest()和fetch仅能在H5中使用。
  5. splitIntoChunks(arrayBuffer, chunkSize)分割文件。 ArrayBuffer是原始二进制数据容器,类似于 "内存区域" 或 "二进制数据仓库",本身不能直接访问数据,需要"视图"来读写数据。 Uint8Array是访问 ArrayBuffer 的 "窗口" 或 "接口",提供了读写 ArrayBuffer 中数据的方法。
  6. fillArray(Math.ceil(totalLength / chunkSize))是我这里为了生存数据帧起始标志用的,[1,2,3,4,5,…,FFFF]。
  7. arrayBufferToBase64(buffer) 可以把buffer数据转为base64,我这里虽然给出了这个方法,但最后只是测试了一下,没有真正取用,是因为打印出来的数据不如十六进制字符串方便查看
  8. 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('=== 解密完成 ===')
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐