/** * OTA 模块 * 功能: * - HTTP 在线升级和本地文件升级 * - 自动 MD5 完整性验证 * - 升级前磁盘空间验证 * 使用方法: * 1. 将代码构建为应用包。在 VSCode DejaOS 插件中点击 `Package` 生成 .temp 文件夹中的 .dpk 文件。 * 2. 将 .dpk 文件(zip 格式)上传到 web 服务器并获取下载 URL。 * 3. 将下载 URL 和 MD5 校验和发送到设备应用。 * - 将 URL 和 MD5 编码为二维码供设备扫描, * - 或使用其他方法(蓝牙、MQTT、RS485 等)。 * 4. 设备下载并使用 MD5 验证包完整性。 * 5. 将包提取到稳定目录并重启设备。 * 6. 重启后,DejaOS 提取包并覆盖现有代码。 * 文档/示例:https://github.com/DejaOS/DejaOS */ import log from './dxLogger.js' import com from './dxCommon.js' import http from './dxHttpClient.js' import * as os from 'os'; const ota = {} ota.UPGRADE_TARGET = '/upgrades.zip' ota.UPGRADE_TEMP = '/upgrades.temp' ota.DF_CMD = `df -k / | awk 'NR==2 {print $4}'` /** * 通过 HTTP 下载升级包 * @param {string} url 必填。下载升级包的 HTTP URL * @param {string} md5 必填。用于完整性验证的 MD5 哈希值(32 位小写十六进制) * @param {number} timeout 可选。下载超时时间(秒),默认:60 * @param {number} size 可选。用于磁盘空间验证的包大小(KB) * @param {Object} [httpOpts] 额外的请求选项 */ ota.updateHttp = function (url, md5, timeout = 60, size, httpOpts) { if (!url || !md5) { throw new Error("'url' 和 'md5' 参数是必填的") } if (size && (typeof size != 'number')) { throw new Error("'size' 参数必须是数字") } // 1. 检查可用磁盘空间 checkDiskSpace(size) // 2. 下载文件到临时目录 com.systemBrief(`rm -rf ${ota.UPGRADE_TARGET} && rm -rf ${ota.UPGRADE_TEMP} `) // 清理现有文件 log.info("download url:" + url) let downloadRet = http.download(url, ota.UPGRADE_TEMP, timeout * 1000, httpOpts) log.info("download result:" + JSON.stringify(downloadRet)) let fileExist = (os.stat(ota.UPGRADE_TEMP)[1] === 0) if (!fileExist) { com.systemBrief(`rm -rf ${ota.UPGRADE_TARGET} && rm -rf ${ota.UPGRADE_TEMP} `) log.info("下载失败。 url:" + url) throw new Error('下载失败。请检查 URL: ' + url) } log.info("verify md5:" + md5) log.info("verify md5 result:" + verifyMD5(ota.UPGRADE_TEMP, md5)) // 3. 验证 MD5 校验和 if (!verifyMD5(ota.UPGRADE_TEMP, md5)) { com.systemBrief(`rm -rf ${ota.UPGRADE_TARGET} && rm -rf ${ota.UPGRADE_TEMP} `) throw new Error('MD5 验证失败') } // 4. 将验证通过的包移动到升级目录 com.systemBrief(`mv ${ota.UPGRADE_TEMP} ${ota.UPGRADE_TARGET} `) com.systemBrief(`sync`) } /** * 从本地文件升级 * 当你已经通过自定义方法下载了包时使用此方法。 * @param {string} path 必填。升级包的路径 * @param {string} md5 必填。用于完整性验证的 MD5 哈希值(32 位小写十六进制) * @param {number} size 可选。用于磁盘空间验证的包大小(KB) */ ota.updateFile = function (path, md5, size) { if (!path || !md5) { throw new Error("'path' 和 'md5' 参数是必填的") } if (size && (typeof size != 'number')) { throw new Error("'size' 参数必须是数字") } let fileExist = (os.stat(path)[1] === 0) if (!fileExist) { throw new Error('文件未找到: ' + path) } // 1. 检查可用磁盘空间 checkDiskSpace(size) // 2. 验证 MD5 校验和 // if (!verifyMD5(path, md5)) { // throw new Error('MD5 验证失败') // } // 3. 将包移动到升级目录 com.systemBrief(`mv ${path} ${ota.UPGRADE_TARGET} `) com.systemBrief(`sync`) } function checkDiskSpace(requiredKb) { if (requiredKb) { const df = parseInt(com.systemWithRes(ota.DF_CMD, 1000)) if (df < 3 * requiredKb) { throw new Error('升级磁盘空间不足') } } } function verifyMD5(filePath, expectedMD5) { const hash = com.md5HashFile(filePath) const actualMD5 = hash.map(v => v.toString(16).padStart(2, '0')).join('') log.info("actualMD5:" + actualMD5) return actualMD5 === expectedMD5 } /** * 触发设备重启 * 在成功升级后调用此方法以应用更改。 */ ota.reboot = function () { com.asyncReboot(2) } //-------------------------已废弃------------------- ota.OTA_ROOT = '/ota' ota.OTA_RUN = ota.OTA_ROOT + '/run.sh' /** * @deprecated 使用 updateHttp() 代替 * 支持自定义脚本的旧升级方法。 * 下载、提取并执行自定义升级脚本。 * @param {string} url 必填。下载升级包的 HTTP URL * @param {string} md5 必填。用于完整性验证的 MD5 哈希值(32 位小写十六进制) * @param {number} size 可选。用于磁盘空间验证的包大小(KB) * @param {string} shell 可选。自定义升级脚本内容 * @param {number} timeout 可选。连接超时时间(秒),默认:3 */ ota.update = function (url, md5, size, shell, timeout = 3) { if (!url || !md5) { throw new Error("'url' 和 'md5' 参数是必填的") } if (size && (typeof size != 'number')) { throw new Error("'size' 参数必须是数字") } // 1. 检查可用磁盘空间 let df = parseInt(com.systemWithRes(ota.DF_CMD, 1000)) if (size) { if (df < (3 * size)) { // 需要 3 倍包大小用于提取 throw new Error('升级磁盘空间不足') } } // 2. 下载到特定目录 const firmware = ota.OTA_ROOT + '/download.zip' const temp = ota.OTA_ROOT + '/temp' com.systemBrief(`rm -rf ${ota.OTA_ROOT} && mkdir ${ota.OTA_ROOT} `) // 清理并创建目录 let download = `wget --no-check-certificate --timeout=${timeout} -c "${url}" -O ${firmware} 2>&1` com.systemBrief(download, 1000) let fileExist = (os.stat(firmware)[1] === 0) let downloadRet if (!fileExist) { downloadRet = http.download(url, firmware, timeout * 1000) } fileExist = (os.stat(firmware)[1] === 0) if (!fileExist) { log.error("download result" + downloadRet) throw new Error('下载失败。请检查 URL: ' + url) } // 3. 验证 MD5 校验和 let md5Hash = com.md5HashFile(firmware) md5Hash = md5Hash.map(v => v.toString(16).padStart(2, 0)).join('') if (md5Hash != md5) { log.error("download result" + downloadRet) throw new Error('MD5 验证失败') } // 4. 提取包 com.systemBrief(`mkdir ${temp} && unzip -o ${firmware} -d ${temp}`) // 5. 如果存在,执行自定义升级脚本 const custom_update = temp + '/custom_update.sh' if (os.stat(custom_update)[1] === 0) { com.systemBrief(`chmod +x ${custom_update}`) com.systemWithRes(`${custom_update}`) } // 6. 创建升级脚本 if (!shell) { // 默认:复制文件并清理 shell = `cp -r ${temp}/* /app/code \n rm -rf ${ota.OTA_ROOT}` } com.systemBrief(`echo "${shell}" > ${ota.OTA_RUN} && chmod +x ${ota.OTA_RUN}`) fileExist = (os.stat(ota.OTA_RUN)[1] === 0) if (!fileExist) { throw new Error('创建升级脚本失败') } com.systemWithRes(`${ota.OTA_RUN}`) } /** * @deprecated 使用 updateHttp() 代替 * 用于 tar.xz 包的旧资源升级。 * 专门用于仅升级资源文件。 * @param {string} url 必填。下载升级包的 HTTP URL * @param {string} md5 必填。用于完整性验证的 MD5 哈希值(32 位小写十六进制) * @param {number} size 可选。用于磁盘空间验证的包大小(KB) * @param {string} shell 可选。自定义升级脚本内容 * @param {number} timeout 可选。连接超时时间(秒),默认:3 */ ota.updateResource = function (url, md5, size, shell, timeout = 3) { if (!url || !md5) { throw new Error("'url' 和 'md5' 参数是必填的") } if (size && (typeof size != 'number')) { throw new Error("'size' 参数必须是数字") } // 1. 检查可用磁盘空间 let df = parseInt(com.systemWithRes(ota.DF_CMD, 1000)) if (size) { if (df < (3 * size)) { // 需要 3 倍包大小用于提取 throw new Error('升级磁盘空间不足') } } // 2. 下载到特定目录 const firmware = ota.OTA_ROOT + '/download.tar.xz' const temp = ota.OTA_ROOT + '/temp' com.systemBrief(`rm -rf ${ota.OTA_ROOT} && mkdir ${ota.OTA_ROOT} `) // 清理并创建目录 let download = `wget --no-check-certificate --timeout=${timeout} -c "${url}" -O ${firmware} 2>&1` com.systemBrief(download, 1000) let fileExist = (os.stat(firmware)[1] === 0) if (!fileExist) { http.download(url, firmware, timeout * 1000) } fileExist = (os.stat(firmware)[1] === 0) if (!fileExist) { throw new Error('下载失败。请检查 URL: ' + url) } // 3. 验证 MD5 校验和 let md5Hash = com.md5HashFile(firmware) md5Hash = md5Hash.map(v => v.toString(16).padStart(2, 0)).join('') if (md5Hash != md5) { throw new Error('MD5 验证失败') } // 4. 提取 tar.xz 包 com.systemBrief(`mkdir ${temp} && tar -xJvf ${firmware} -C ${temp}`) // 5. 创建资源升级脚本 if (!shell) { shell = ` source=${temp}/vgapp/res/image/bk.png target=/app/code/resource/image/bg.png if test -e "\\$source"; then cp "\\$source" "\\$target" fi source=${temp}/vgapp/res/image/bk_90.png target=/app/code/resource/image/bg_90.png if test -e "\\$source"; then cp "\\$source" "\\$target" fi source=${temp}/vgapp/res/font/AlibabaPuHuiTi-2-65-Medium.ttf target=/app/code/resource/font.ttf if test -e "\\$source"; then cp "\\$source" "\\$target" fi source=${temp}/vgapp/wav/*.wav target=/app/code/resource/wav/ cp "\\$source" "\\$target" rm -rf ${ota.OTA_ROOT} ` } com.systemBrief(`echo "${shell}" > ${ota.OTA_RUN} && chmod +x ${ota.OTA_RUN}`) fileExist = (os.stat(ota.OTA_RUN)[1] === 0) if (!fileExist) { throw new Error('创建升级脚本失败') } com.systemWithRes(`${ota.OTA_RUN}`) } export default ota