| | |
| | | /** |
| | | * OTA Module |
| | | * Features: |
| | | * - HTTP online and local file upgrades |
| | | * - Automatic MD5 integrity verification |
| | | * - Pre-upgrade disk space validation |
| | | * OTA 模块 |
| | | * 功能: |
| | | * - HTTP 在线升级和本地文件升级 |
| | | * - 自动 MD5 完整性验证 |
| | | * - 升级前磁盘空间验证 |
| | | |
| | | * Usage: |
| | | 1. Build code into an app package. Click `Package` in VSCode DejaOS Plugin to generate a .dpk file in .temp folder. |
| | | 2. Upload the .dpk file (zip format) to a web server and get the download URL. |
| | | 3. Send the download URL and MD5 checksum to the device app. |
| | | - Encode URL and MD5 as QR code for device scanning, |
| | | - Or use other methods (Bluetooth, MQTT, RS485, etc.). |
| | | 4. Device downloads and verifies package integrity using MD5. |
| | | 5. Extract package to stable directory and reboot device. |
| | | 6. After reboot, DejaOS extracts package and overwrites existing code. |
| | | * Doc/Demo: https://github.com/DejaOS/DejaOS |
| | | * 使用方法: |
| | | * 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' |
| | |
| | | ota.UPGRADE_TEMP = '/upgrades.temp' |
| | | ota.DF_CMD = `df -k / | awk 'NR==2 {print $4}'` |
| | | /** |
| | | * Download upgrade package via HTTP |
| | | * @param {string} url Required. HTTP URL for downloading the upgrade package |
| | | * @param {string} md5 Required. MD5 hash for integrity verification (32-char lowercase hex) |
| | | * @param {number} timeout Optional. Download timeout in seconds (default: 60) |
| | | * @param {number} size Optional. Package size in KB for disk space validation |
| | | * @param {Object} [httpOpts] Additional request opts |
| | | * 通过 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' and 'md5' parameters are required") |
| | | throw new Error("'url' 和 'md5' 参数是必填的") |
| | | } |
| | | if (size && (typeof size != 'number')) { |
| | | throw new Error("'size' parameter must be a number") |
| | | throw new Error("'size' 参数必须是数字") |
| | | } |
| | | // 1. Check available disk space |
| | | // 1. 检查可用磁盘空间 |
| | | checkDiskSpace(size) |
| | | // 2. Download file to temporary directory |
| | | com.systemBrief(`rm -rf ${ota.UPGRADE_TARGET} && rm -rf ${ota.UPGRADE_TEMP} `) // Clean up existing files |
| | | // 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} `) |
| | | throw new Error('Download failed. Please check the URL: ' + url) |
| | | log.info("下载失败。 url:" + url) |
| | | throw new Error('下载失败。请检查 URL: ' + url) |
| | | } |
| | | // 3. Verify MD5 checksum |
| | | |
| | | 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 verification failed') |
| | | throw new Error('MD5 验证失败') |
| | | } |
| | | // 4. Move verified package to upgrade directory |
| | | // 4. 将验证通过的包移动到升级目录 |
| | | com.systemBrief(`mv ${ota.UPGRADE_TEMP} ${ota.UPGRADE_TARGET} `) |
| | | com.systemBrief(`sync`) |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Upgrade from local file |
| | | * Use this when you've already downloaded the package via custom methods. |
| | | * @param {string} path Required. Path to the upgrade package |
| | | * @param {string} md5 Required. MD5 hash for integrity verification (32-char lowercase hex) |
| | | * @param {number} size Optional. Package size in KB for disk space validation |
| | | * 从本地文件升级 |
| | | * 当你已经通过自定义方法下载了包时使用此方法。 |
| | | * @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' and 'md5' parameters are required") |
| | | throw new Error("'path' 和 'md5' 参数是必填的") |
| | | } |
| | | if (size && (typeof size != 'number')) { |
| | | throw new Error("'size' parameter must be a number") |
| | | throw new Error("'size' 参数必须是数字") |
| | | } |
| | | let fileExist = (os.stat(path)[1] === 0) |
| | | if (!fileExist) { |
| | | throw new Error('File not found: ' + path) |
| | | throw new Error('文件未找到: ' + path) |
| | | } |
| | | // 1. Check available disk space |
| | | // 1. 检查可用磁盘空间 |
| | | checkDiskSpace(size) |
| | | |
| | | // 2. Verify MD5 checksum |
| | | if (!verifyMD5(path, md5)) { |
| | | throw new Error('MD5 verification failed') |
| | | } |
| | | // 2. 验证 MD5 校验和 |
| | | // if (!verifyMD5(path, md5)) { |
| | | // throw new Error('MD5 验证失败') |
| | | // } |
| | | |
| | | // 3. Move package to upgrade directory |
| | | // 3. 将包移动到升级目录 |
| | | com.systemBrief(`mv ${path} ${ota.UPGRADE_TARGET} `) |
| | | com.systemBrief(`sync`) |
| | | } |
| | |
| | | if (requiredKb) { |
| | | const df = parseInt(com.systemWithRes(ota.DF_CMD, 1000)) |
| | | if (df < 3 * requiredKb) { |
| | | throw new Error('Insufficient disk space for upgrade') |
| | | 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 |
| | | } |
| | | /** |
| | | * Trigger device reboot |
| | | * Call this after successful upgrade to apply changes. |
| | | * 触发设备重启 |
| | | * 在成功升级后调用此方法以应用更改。 |
| | | */ |
| | | ota.reboot = function () { |
| | | com.asyncReboot(2) |
| | | } |
| | | //-------------------------DEPRECATED------------------- |
| | | //-------------------------已废弃------------------- |
| | | ota.OTA_ROOT = '/ota' |
| | | ota.OTA_RUN = ota.OTA_ROOT + '/run.sh' |
| | | |
| | | /** |
| | | * @deprecated Use updateHttp() instead |
| | | * Legacy upgrade method with custom script support. |
| | | * Downloads, extracts, and executes custom upgrade scripts. |
| | | * @param {string} url Required. HTTP URL for downloading the upgrade package |
| | | * @param {string} md5 Required. MD5 hash for integrity verification (32-char lowercase hex) |
| | | * @param {number} size Optional. Package size in KB for disk space validation |
| | | * @param {string} shell Optional. Custom upgrade script content |
| | | * @param {number} timeout Optional. Connection timeout in seconds (default: 3) |
| | | * @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' and 'md5' parameters are required") |
| | | throw new Error("'url' 和 'md5' 参数是必填的") |
| | | } |
| | | if (size && (typeof size != 'number')) { |
| | | throw new Error("'size' parameter must be a number") |
| | | throw new Error("'size' 参数必须是数字") |
| | | } |
| | | // 1. Check available disk space |
| | | // 1. 检查可用磁盘空间 |
| | | let df = parseInt(com.systemWithRes(ota.DF_CMD, 1000)) |
| | | if (size) { |
| | | if (df < (3 * size)) { // Require 3x package size for extraction |
| | | throw new Error('Insufficient disk space for upgrade') |
| | | if (df < (3 * size)) { // 需要 3 倍包大小用于提取 |
| | | throw new Error('升级磁盘空间不足') |
| | | } |
| | | } |
| | | // 2. Download to specific directory |
| | | // 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} `) // Clean and create directory |
| | | 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) |
| | |
| | | fileExist = (os.stat(firmware)[1] === 0) |
| | | if (!fileExist) { |
| | | log.error("download result" + downloadRet) |
| | | throw new Error('Download failed. Please check the URL: ' + url) |
| | | throw new Error('下载失败。请检查 URL: ' + url) |
| | | } |
| | | // 3. Verify MD5 checksum |
| | | // 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 verification failed') |
| | | throw new Error('MD5 验证失败') |
| | | } |
| | | // 4. Extract package |
| | | // 4. 提取包 |
| | | com.systemBrief(`mkdir ${temp} && unzip -o ${firmware} -d ${temp}`) |
| | | // 5. Execute custom upgrade script if present |
| | | // 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. Create upgrade script |
| | | // 6. 创建升级脚本 |
| | | if (!shell) { |
| | | // Default: copy files and clean up |
| | | // 默认:复制文件并清理 |
| | | 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('Failed to create upgrade script') |
| | | throw new Error('创建升级脚本失败') |
| | | } |
| | | com.systemWithRes(`${ota.OTA_RUN}`) |
| | | } |
| | | |
| | | /** |
| | | * @deprecated Use updateHttp() instead |
| | | * Legacy resource upgrade for tar.xz packages. |
| | | * Specialized for upgrading resource files only. |
| | | * @param {string} url Required. HTTP URL for downloading the upgrade package |
| | | * @param {string} md5 Required. MD5 hash for integrity verification (32-char lowercase hex) |
| | | * @param {number} size Optional. Package size in KB for disk space validation |
| | | * @param {string} shell Optional. Custom upgrade script content |
| | | * @param {number} timeout Optional. Connection timeout in seconds (default: 3) |
| | | * @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' and 'md5' parameters are required") |
| | | throw new Error("'url' 和 'md5' 参数是必填的") |
| | | } |
| | | if (size && (typeof size != 'number')) { |
| | | throw new Error("'size' parameter must be a number") |
| | | throw new Error("'size' 参数必须是数字") |
| | | } |
| | | // 1. Check available disk space |
| | | // 1. 检查可用磁盘空间 |
| | | let df = parseInt(com.systemWithRes(ota.DF_CMD, 1000)) |
| | | if (size) { |
| | | if (df < (3 * size)) { // Require 3x package size for extraction |
| | | throw new Error('Insufficient disk space for upgrade') |
| | | if (df < (3 * size)) { // 需要 3 倍包大小用于提取 |
| | | throw new Error('升级磁盘空间不足') |
| | | } |
| | | } |
| | | // 2. Download to specific directory |
| | | // 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} `) // Clean and create directory |
| | | 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) |
| | |
| | | } |
| | | fileExist = (os.stat(firmware)[1] === 0) |
| | | if (!fileExist) { |
| | | throw new Error('Download failed. Please check the URL: ' + url) |
| | | throw new Error('下载失败。请检查 URL: ' + url) |
| | | } |
| | | |
| | | // 3. Verify MD5 checksum |
| | | // 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 verification failed') |
| | | throw new Error('MD5 验证失败') |
| | | } |
| | | // 4. Extract tar.xz package |
| | | // 4. 提取 tar.xz 包 |
| | | com.systemBrief(`mkdir ${temp} && tar -xJvf ${firmware} -C ${temp}`) |
| | | // 5. Create resource upgrade script |
| | | // 5. 创建资源升级脚本 |
| | | if (!shell) { |
| | | shell = ` |
| | | source=${temp}/vgapp/res/image/bk.png |
| | |
| | | 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('Failed to create upgrade script') |
| | | throw new Error('创建升级脚本失败') |
| | | } |
| | | com.systemWithRes(`${ota.OTA_RUN}`) |
| | | } |