vf107/dxmodules/dxOta.js
@@ -1,20 +1,20 @@
/**
 * 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'
@@ -26,24 +26,24 @@
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))
@@ -51,46 +51,50 @@
    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`)
}
@@ -99,7 +103,7 @@
    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('升级磁盘空间不足')
        }
    }
}
@@ -107,47 +111,48 @@
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)
@@ -158,65 +163,65 @@
    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)
@@ -225,18 +230,18 @@
    }
    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
@@ -264,7 +269,7 @@
    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}`)
}