/**
|
* 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
|