feat(ota): support JSON progress updates
This commit is contained in:
@@ -1,17 +1,20 @@
|
|||||||
import type { DeviceFirmwareStatus } from '@/services/device'
|
import type { DeviceFirmwareStatus } from '@/services/device'
|
||||||
import { isFirmwareCurrent, isFirmwareTargetReached, isFirmwareUpdateInProgress } from '../../utils/firmware-status'
|
import { isFirmwareCurrent, isFirmwareTargetReached, isFirmwareUpdateInProgress, normalizeFirmwareStatus } from '../../utils/firmware-status'
|
||||||
|
|
||||||
export function firmwareProgressView(status: DeviceFirmwareStatus | null, refreshError = false) {
|
export function firmwareProgressView(status: DeviceFirmwareStatus | null, refreshError = false) {
|
||||||
const state = String(status?.update_status || '').trim().toLowerCase()
|
const normalizedStatus = normalizeFirmwareStatus(status)
|
||||||
|
const state = String(normalizedStatus?.update_status || '').trim().toLowerCase()
|
||||||
const complete = state === 'success' || state === 'completed'
|
const complete = state === 'success' || state === 'completed'
|
||||||
|| (isFirmwareUpdateInProgress(state) && isFirmwareTargetReached(status))
|
|| (isFirmwareUpdateInProgress(state) && isFirmwareTargetReached(normalizedStatus))
|
||||||
const current = !complete && !isFirmwareUpdateInProgress(state) && isFirmwareCurrent(status)
|
const current = !complete && !isFirmwareUpdateInProgress(state) && isFirmwareCurrent(normalizedStatus)
|
||||||
const failed = state === 'failed' && !current
|
const failed = state === 'failed' && !current
|
||||||
const value = Number(status?.progress)
|
const value = Number(normalizedStatus?.progress)
|
||||||
const progress = complete ? 100 : Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0
|
const progress = complete ? 100 : Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0
|
||||||
const hasProgress = complete || progress > 0
|
const hasProgress = complete || progress > 0
|
||||||
const target = status?.target_version
|
// MQTT 003 carries target_version; latest_version is display-only fallback
|
||||||
const version = current ? status?.current_version : target && !['unknown', 'updating'].includes(target) ? target : status?.latest_version
|
// for older status rows and must not be used for completion detection.
|
||||||
|
const target = normalizedStatus?.target_version
|
||||||
|
const version = current ? normalizedStatus?.current_version : target && !['unknown', 'updating'].includes(target) ? target : normalizedStatus?.latest_version
|
||||||
const title = complete ? '固件更新完成' : current ? '当前已是最新版本' : failed ? '固件更新失败' : '固件更新中'
|
const title = complete ? '固件更新完成' : current ? '当前已是最新版本' : failed ? '固件更新失败' : '固件更新中'
|
||||||
const message = complete ? '设备已完成更新,可以继续使用。'
|
const message = complete ? '设备已完成更新,可以继续使用。'
|
||||||
: current ? '当前设备已运行最新版本,无需更新。'
|
: current ? '当前设备已运行最新版本,无需更新。'
|
||||||
|
|||||||
@@ -8,6 +8,20 @@ export function isFirmwareUpdateInProgress(status?: string | null): boolean {
|
|||||||
return ['sent', 'accepted', 'updating'].includes(normalizeFirmwareUpdateStatus(status))
|
return ['sent', 'accepted', 'updating'].includes(normalizeFirmwareUpdateStatus(status))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeFirmwareStatus(status: DeviceFirmwareStatus | null): DeviceFirmwareStatus | null {
|
||||||
|
if (!status) return null
|
||||||
|
|
||||||
|
const numericProgress = Number(status.progress)
|
||||||
|
const progress = Number.isFinite(numericProgress)
|
||||||
|
? Math.max(0, Math.min(100, numericProgress))
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
...status,
|
||||||
|
progress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function firmwareVersionParts(value?: string | null): number[] | null {
|
function firmwareVersionParts(value?: string | null): number[] | null {
|
||||||
const normalized = String(value || '').trim().toLowerCase()
|
const normalized = String(value || '').trim().toLowerCase()
|
||||||
if (!normalized || ['unknown', 'updating', '0.0.0', '--'].includes(normalized)) return null
|
if (!normalized || ['unknown', 'updating', '0.0.0', '--'].includes(normalized)) return null
|
||||||
@@ -36,15 +50,16 @@ export function isFirmwareCurrent(status: DeviceFirmwareStatus | null): boolean
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function firmwareSummaryView(status: DeviceFirmwareStatus | null) {
|
export function firmwareSummaryView(status: DeviceFirmwareStatus | null) {
|
||||||
if (!status) return { action: '暂不可用', subtitle: '暂时无法获取版本,请稍后重试' }
|
const normalizedStatus = normalizeFirmwareStatus(status)
|
||||||
if (isFirmwareUpdateInProgress(status.update_status) && !isFirmwareTargetReached(status)) {
|
if (!normalizedStatus) return { action: '暂不可用', subtitle: '暂时无法获取版本,请稍后重试' }
|
||||||
|
if (isFirmwareUpdateInProgress(normalizedStatus.update_status) && !isFirmwareTargetReached(normalizedStatus)) {
|
||||||
return { action: '查看进度', subtitle: '设备正在更新,请稍候' }
|
return { action: '查看进度', subtitle: '设备正在更新,请稍候' }
|
||||||
}
|
}
|
||||||
if (isFirmwareCurrent(status)) return { action: '已最新', subtitle: '当前已是最新版本' }
|
if (isFirmwareCurrent(normalizedStatus)) return { action: '已最新', subtitle: '当前已是最新版本' }
|
||||||
if (normalizeFirmwareUpdateStatus(status.update_status) === 'failed') {
|
if (normalizeFirmwareUpdateStatus(normalizedStatus.update_status) === 'failed') {
|
||||||
return { action: status.update_available ? '更新' : '版本待确认', subtitle: status.can_update ? '更新失败,可重试' : '上次更新失败,当前版本待确认' }
|
return { action: normalizedStatus.update_available ? '更新' : '版本待确认', subtitle: normalizedStatus.can_update ? '更新失败,可重试' : '上次更新失败,当前版本待确认' }
|
||||||
}
|
}
|
||||||
return status.update_available
|
return normalizedStatus.update_available
|
||||||
? { action: '更新', subtitle: '发现可更新版本' }
|
? { action: '更新', subtitle: '发现可更新版本' }
|
||||||
: { action: '版本待确认', subtitle: '暂无完整版本信息' }
|
: { action: '版本待确认', subtitle: '暂无完整版本信息' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,22 @@ test('received progress is used directly, including fractional values', () => {
|
|||||||
assert.equal(view.percent, 65)
|
assert.equal(view.percent, 65)
|
||||||
assert.equal(view.complete, false)
|
assert.equal(view.complete, false)
|
||||||
})
|
})
|
||||||
|
test('MQTT 003 target_version is shown while progress is updating', () => {
|
||||||
|
const view = firmwareProgressView({
|
||||||
|
...status('updating', 30),
|
||||||
|
latest_version: '2.0.4',
|
||||||
|
target_version: '2.0.4',
|
||||||
|
})
|
||||||
|
assert.equal(view.version, '2.0.4')
|
||||||
|
assert.equal(view.progress, 30)
|
||||||
|
assert.equal(view.percent, 30)
|
||||||
|
assert.match(view.message, /保持设备电量充足/)
|
||||||
|
})
|
||||||
|
test('active status without target_version falls back to latest_version', () => {
|
||||||
|
const view = firmwareProgressView(status('updating', 30))
|
||||||
|
assert.equal(view.version, '1.8.2')
|
||||||
|
assert.equal(view.progress, 30)
|
||||||
|
})
|
||||||
test('nearly complete progress never rounds up to 100', () => {
|
test('nearly complete progress never rounds up to 100', () => {
|
||||||
assert.equal(firmwareProgressView(status('updating', 99.9)).percent, 99)
|
assert.equal(firmwareProgressView(status('updating', 99.9)).percent, 99)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -127,6 +127,21 @@ Headers:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
设备通过 MQTT `msg_id=003` 上报实时进度时,使用 JSON `data.target_version` 和 `data.progress`,例如:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"msg_id": "003",
|
||||||
|
"status": "updating",
|
||||||
|
"data": {
|
||||||
|
"target_version": "2.0.4",
|
||||||
|
"progress": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
服务器保存该进度,小程序继续通过本状态接口轮询显示。`progress=100` 只表示设备报告写入进度完成,设备重启后上报的实际版本仍是最终完成依据。
|
||||||
|
|
||||||
### 4. 管理固件配置信息(管理员接口)
|
### 4. 管理固件配置信息(管理员接口)
|
||||||
|
|
||||||
**获取固件配置**
|
**获取固件配置**
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# OTA `003` 实时进度协议
|
||||||
|
|
||||||
|
设备通过 `device/{device_id}/response` 上报 OTA 状态。服务器下发 OTA 命令仍使用 `msg_id=003`,设备回执使用相同的 `msg_id`。
|
||||||
|
|
||||||
|
## 进行中回执
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"msg_id": "003",
|
||||||
|
"status": "updating",
|
||||||
|
"data": {
|
||||||
|
"target_version": "2.0.4",
|
||||||
|
"progress": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`progress` 表示设备当前已完成的百分比,范围为 `0` 到 `100`。设备应在状态变化时上报,并避免高频重复发送。`target_version` 应与服务器下发任务的目标版本一致。
|
||||||
|
|
||||||
|
## 状态约定
|
||||||
|
|
||||||
|
| 状态 | 必要字段 | 服务器行为 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `accepted` | `current_version`、`target_version` | 记录设备已接受任务,进度默认为 `0` |
|
||||||
|
| `updating` | `target_version`、`progress` | 保存最新进度并供小程序查询 |
|
||||||
|
| `success` | `new_version` 或 `target_version` | 记录完成,进度置为 `100`,随后以设备重启后的版本上报作为最终确认 |
|
||||||
|
| `failed` | `target_version`、可选 `progress` | 保存失败状态和最后有效进度 |
|
||||||
|
|
||||||
|
服务器兼容旧设备上报的 `version`、`new_version` 字段,并优先使用 `target_version` 作为本次任务目标。非法进度值不会覆盖已保存的有效进度。
|
||||||
|
|
||||||
|
小程序通过设备固件状态接口轮询 `update_status`、`progress` 和 `target_version`,显示最近一次服务器收到的设备进度。进度达到 `100` 仍显示为等待设备确认,直到设备重启后上报目标版本。
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Awaitable, Callable, Dict, Optional
|
from typing import Awaitable, Callable, Dict, Optional
|
||||||
@@ -311,26 +312,39 @@ class TalkingQMQTTService:
|
|||||||
|
|
||||||
async def _handle_ota_response(self, device_id: str, payload: dict):
|
async def _handle_ota_response(self, device_id: str, payload: dict):
|
||||||
status = payload.get("status")
|
status = payload.get("status")
|
||||||
data = payload.get("data", {})
|
data = payload.get("data") or {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
logger.warning(device_id, "", f"[OTA] invalid response data: {payload}")
|
||||||
|
return
|
||||||
|
|
||||||
|
target_version = data.get("target_version") or data.get("version") or data.get("new_version")
|
||||||
progress = data.get("progress")
|
progress = data.get("progress")
|
||||||
try:
|
try:
|
||||||
progress_value = float(progress) if progress is not None else None
|
parsed_progress = float(progress) if progress is not None else None
|
||||||
|
progress_value = (
|
||||||
|
parsed_progress
|
||||||
|
if parsed_progress is not None and math.isfinite(parsed_progress) and 0 <= parsed_progress <= 100
|
||||||
|
else None
|
||||||
|
)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
progress_value = None
|
progress_value = None
|
||||||
|
|
||||||
|
if progress is not None and progress_value is None:
|
||||||
|
logger.warning(device_id, "", f"[OTA] invalid progress value: {progress!r}")
|
||||||
|
|
||||||
if status == "accepted":
|
if status == "accepted":
|
||||||
current = data.get("current_version")
|
current = data.get("current_version")
|
||||||
target = data.get("target_version")
|
logger.info(device_id, "", f"[OTA] 设备 {device_id} 已接受升级: {current} -> {target_version}")
|
||||||
logger.info(device_id, "", f"[OTA] 设备 {device_id} 已接受升级: {current} -> {target}")
|
|
||||||
|
|
||||||
await self._schedule_persistence(
|
await self._schedule_persistence(
|
||||||
device_id,
|
device_id,
|
||||||
"ota",
|
"ota",
|
||||||
device_firmware_update_manager.update_firmware_update(
|
device_firmware_update_manager.update_firmware_update(
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
firmware_version=data.get("target_version") or data.get("version") or data.get("new_version") or "unknown",
|
firmware_version=target_version or "unknown",
|
||||||
update_status="accepted",
|
update_status="accepted",
|
||||||
progress=progress_value if progress_value is not None else 0.0,
|
progress=progress_value if progress_value is not None else 0.0,
|
||||||
|
target_version=target_version,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
elif status == "updating":
|
elif status == "updating":
|
||||||
@@ -339,13 +353,14 @@ class TalkingQMQTTService:
|
|||||||
"ota",
|
"ota",
|
||||||
device_firmware_update_manager.update_firmware_update(
|
device_firmware_update_manager.update_firmware_update(
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
firmware_version=data.get("version") or data.get("new_version") or "updating",
|
firmware_version=target_version or "updating",
|
||||||
update_status="updating",
|
update_status="updating",
|
||||||
progress=progress_value,
|
progress=progress_value,
|
||||||
|
target_version=target_version,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
elif status == "success":
|
elif status == "success":
|
||||||
new_version = data.get("new_version") or data.get("version")
|
new_version = data.get("new_version") or data.get("version") or data.get("target_version")
|
||||||
logger.info(device_id, "", f"[OTA] 设备 {device_id} 升级完成: {new_version}")
|
logger.info(device_id, "", f"[OTA] 设备 {device_id} 升级完成: {new_version}")
|
||||||
|
|
||||||
await self._schedule_persistence(
|
await self._schedule_persistence(
|
||||||
@@ -367,6 +382,7 @@ class TalkingQMQTTService:
|
|||||||
firmware_version=new_version or "unknown",
|
firmware_version=new_version or "unknown",
|
||||||
update_status="success",
|
update_status="success",
|
||||||
progress=100.0,
|
progress=100.0,
|
||||||
|
target_version=target_version,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
elif status == "failed":
|
elif status == "failed":
|
||||||
@@ -375,9 +391,10 @@ class TalkingQMQTTService:
|
|||||||
"ota",
|
"ota",
|
||||||
device_firmware_update_manager.update_firmware_update(
|
device_firmware_update_manager.update_firmware_update(
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
firmware_version=data.get("version") or data.get("new_version") or "unknown",
|
firmware_version=target_version or "unknown",
|
||||||
update_status="failed",
|
update_status="failed",
|
||||||
progress=progress_value,
|
progress=progress_value,
|
||||||
|
target_version=target_version,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
logger.warning(device_id, "", f"[OTA] command failed: {payload}")
|
logger.warning(device_id, "", f"[OTA] command failed: {payload}")
|
||||||
|
|||||||
@@ -146,9 +146,15 @@ async def test_simulated_device_force_ota_dispatches_once_and_completes(monkeypa
|
|||||||
|
|
||||||
await mqtt_service._handle_ota_response(
|
await mqtt_service._handle_ota_response(
|
||||||
device_id,
|
device_id,
|
||||||
{"status": "updating", "data": {"version": "1.8.5", "progress": 65}},
|
{
|
||||||
|
"msg_id": "003",
|
||||||
|
"status": "updating",
|
||||||
|
"data": {"target_version": "1.8.5", "progress": 65},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert record["update_status"] == "updating"
|
assert record["update_status"] == "updating"
|
||||||
|
assert record["firmware_version"] == "1.8.5"
|
||||||
|
assert record["target_version"] == "1.8.5"
|
||||||
assert record["progress"] == 65.0
|
assert record["progress"] == 65.0
|
||||||
|
|
||||||
await mqtt_service._handle_ota_response(
|
await mqtt_service._handle_ota_response(
|
||||||
|
|||||||
Reference in New Issue
Block a user