1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/**
 * 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