Initial commit of akmon project
This commit is contained in:
482
uts/utils/chartDataUtils.uts
Normal file
482
uts/utils/chartDataUtils.uts
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* 健康数据图表处理工具
|
||||
* 提供将原始健康数据转换为ak-charts显示格式的功能
|
||||
*/
|
||||
|
||||
import type { HealthData } from '../../uni_modules/ak-sbsrv/utssdk/interface.uts'
|
||||
import type { ChartOption, ChartType } from '../../uni_modules/ak-charts/interface.uts'
|
||||
|
||||
type MultiMetricBucket = {
|
||||
heart?: number[]
|
||||
spo2?: number[]
|
||||
speed?: number[]
|
||||
steps?: number[]
|
||||
}
|
||||
|
||||
// Re-export ChartOption for use in other modules
|
||||
export type { ChartOption, ChartType }
|
||||
|
||||
/**
|
||||
* 心率数据处理配置
|
||||
*/
|
||||
export type HeartRateChartConfig = {
|
||||
maxPoints?: number; // 最大显示点数,默认50
|
||||
frequency?: number; // 数据频率控制,每N个点取一个,默认1(全部)
|
||||
color?: string; // 图表颜色,默认'#FF6384'
|
||||
}
|
||||
|
||||
/**
|
||||
* 血氧数据处理配置
|
||||
*/
|
||||
export type Spo2ChartConfig = {
|
||||
maxPoints?: number; // 最大显示点数,默认50
|
||||
frequency?: number; // 数据频率控制,默认1
|
||||
color?: string; // 图表颜色,默认'#36A2EB'
|
||||
}
|
||||
|
||||
/**
|
||||
* 步数数据处理配置
|
||||
*/
|
||||
export type StepsChartConfig = {
|
||||
maxPoints?: number; // 最大显示点数,默认50
|
||||
frequency?: number; // 数据频率控制,默认1
|
||||
color?: string; // 图表颜色,默认'#FFCE56'
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心率数据为图表格式
|
||||
* @param healthDataArray 健康数据数组
|
||||
* @param config 配置选项
|
||||
* @returns ChartOption对象
|
||||
*/
|
||||
export function processHeartRateData(
|
||||
healthDataArray: HealthData[],
|
||||
config: HeartRateChartConfig = {}
|
||||
): ChartOption {
|
||||
const {
|
||||
maxPoints = 100, // 增加到100个点
|
||||
frequency = 1,
|
||||
color = '#FF6384'
|
||||
} = config
|
||||
|
||||
// 兼容性:如果传入frequency作为时间间隔(1或5秒),则使用时间分桶
|
||||
// frequency字段 legacy 用法仍然支持按索引采样
|
||||
const freq = frequency
|
||||
const useBucket = freq >= 1 && freq <= 10
|
||||
|
||||
if (!useBucket) {
|
||||
// legacy 行为:按索引采样
|
||||
const heartRateData = healthDataArray
|
||||
.filter(data => data.type === 'heart' && data.heartRate != null)
|
||||
.filter((_, index) => index % frequency === 0)
|
||||
.slice(-maxPoints)
|
||||
|
||||
const data = heartRateData.map(item => item.heartRate!)
|
||||
const labels = heartRateData.map((_, index) => ((index + 1) % 10 === 0 ? `${index + 1}` : ''))
|
||||
return { type: 'line' as ChartType, data, labels, color }
|
||||
}
|
||||
|
||||
// 使用时间分桶(默认1s或5s): 将心率按时间区间取最大值
|
||||
const intervalSeconds = Math.max(1, Math.floor(freq))
|
||||
const intervalMs = intervalSeconds * 1000
|
||||
const buckets = new Map<number, number[]>()
|
||||
for (let i = 0; i < healthDataArray.length; i++) {
|
||||
const d = healthDataArray[i]
|
||||
if (d.type !== 'heart' || d.heartRate == null) continue
|
||||
const timestamp = d.timestamp
|
||||
const ts = (timestamp == null) ? Date.now() : (typeof timestamp === 'number' ? timestamp : new Date(timestamp as string).getTime())
|
||||
const key = Math.floor(ts / intervalMs) * intervalMs
|
||||
if (!buckets.has(key)) buckets.set(key, [])
|
||||
const heartRateValue = d.heartRate != null ? Number.parseFloat(d.heartRate.toString()) : 0
|
||||
buckets.get(key)!.push(heartRateValue)
|
||||
}
|
||||
|
||||
// 按时间排序并只取最新maxPoints
|
||||
const keys: number[] = []
|
||||
buckets.forEach((_, key) => { keys.push(key) })
|
||||
keys.sort((a, b) => a - b)
|
||||
const slicedKeys = keys.slice(-maxPoints)
|
||||
|
||||
const data: number[] = []
|
||||
const labels: string[] = []
|
||||
for (let i = 0; i < slicedKeys.length; i++) {
|
||||
const key = slicedKeys[i]
|
||||
const arr = (buckets.get(key) != null) ? buckets.get(key)! : []
|
||||
let maxValue = NaN
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
const num = arr[j] as number
|
||||
if (isNaN(num)) continue
|
||||
if (isNaN(maxValue) || num > maxValue) {
|
||||
maxValue = num
|
||||
}
|
||||
}
|
||||
data.push(maxValue)
|
||||
const labelDate = new Date(key)
|
||||
const ss = labelDate.getSeconds().toString().padStart(2, '0')
|
||||
const mm = labelDate.getMinutes().toString().padStart(2, '0')
|
||||
labels.push(`${mm}:${ss}`)
|
||||
}
|
||||
return { type: 'line' as ChartType, data, labels, color }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理血氧数据为图表格式
|
||||
* @param healthDataArray 健康数据数组
|
||||
* @param config 配置选项
|
||||
* @returns ChartOption对象
|
||||
*/
|
||||
export function processSpo2Data(
|
||||
healthDataArray: HealthData[],
|
||||
config: Spo2ChartConfig = {}
|
||||
): ChartOption {
|
||||
const {
|
||||
maxPoints = 100, // 增加到100个点
|
||||
frequency = 1,
|
||||
color = '#36A2EB'
|
||||
} = config
|
||||
// 使用时间分桶(如果frequency为秒级1或5)取区间内最大值(或最后值)
|
||||
const freq = frequency
|
||||
const useBucket = freq >= 1 && freq <= 10
|
||||
if (!useBucket) {
|
||||
const spo2Data = healthDataArray
|
||||
.filter(data => data.type === 'spo2' && data.spo2 != null)
|
||||
.filter((_, index) => index % frequency === 0)
|
||||
.slice(-maxPoints)
|
||||
const data = spo2Data.map(item => item.spo2!)
|
||||
const labels = spo2Data.map((_, index) => ((index + 1) % 10 === 0 ? `${index + 1}` : ''))
|
||||
return { type: 'line' as ChartType, data, labels, color }
|
||||
}
|
||||
|
||||
const intervalSeconds = Math.max(1, Math.floor(freq))
|
||||
const intervalMs = intervalSeconds * 1000
|
||||
const buckets = new Map<number, number[]>()
|
||||
for (let i = 0; i < healthDataArray.length; i++) {
|
||||
const d = healthDataArray[i]
|
||||
if (d.type !== 'spo2' || d.spo2 == null) continue
|
||||
const timestamp = d.timestamp
|
||||
const ts = (timestamp == null) ? Date.now() : (typeof timestamp === 'number' ? timestamp : new Date(timestamp as string).getTime())
|
||||
const key = Math.floor(ts / intervalMs) * intervalMs
|
||||
if (!buckets.has(key)) buckets.set(key, [])
|
||||
const spo2Value = d.spo2 != null ? Number.parseFloat(d.spo2.toString()) : 0
|
||||
buckets.get(key)!.push(spo2Value)
|
||||
}
|
||||
const keys: number[] = []
|
||||
buckets.forEach((_, key) => { keys.push(key) })
|
||||
keys.sort((a, b) => a - b)
|
||||
const slicedKeys = keys.slice(-maxPoints)
|
||||
|
||||
const data: number[] = []
|
||||
const labels: string[] = []
|
||||
for (let i = 0; i < slicedKeys.length; i++) {
|
||||
const key = slicedKeys[i]
|
||||
const bucketValue = buckets.get(key)
|
||||
const arr = (bucketValue != null) ? bucketValue : []
|
||||
let maxValue = NaN
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
const num = arr[j] as number
|
||||
if (isNaN(num)) continue
|
||||
if (isNaN(maxValue) || num > maxValue) {
|
||||
maxValue = num
|
||||
}
|
||||
}
|
||||
data.push(maxValue)
|
||||
const labelDate = new Date(key)
|
||||
const ss = labelDate.getSeconds().toString().padStart(2, '0')
|
||||
const mm = labelDate.getMinutes().toString().padStart(2, '0')
|
||||
labels.push(`${mm}:${ss}`)
|
||||
}
|
||||
return { type: 'line' as ChartType, data, labels, color }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理步数数据为图表格式
|
||||
* @param healthDataArray 健康数据数组
|
||||
* @param config 配置选项
|
||||
* @returns ChartOption对象
|
||||
*/
|
||||
export function processStepsData(
|
||||
healthDataArray: HealthData[],
|
||||
config: StepsChartConfig = {}
|
||||
): ChartOption {
|
||||
const {
|
||||
maxPoints = 100, // 增加到100个点
|
||||
frequency = 1,
|
||||
color = '#FFCE56'
|
||||
} = config
|
||||
// 步数通常以累计值或增量形式出现,按时间区间取最后一个值(或累加)
|
||||
const freq = frequency
|
||||
const useBucket = freq >= 1 && freq <= 10
|
||||
if (!useBucket) {
|
||||
const stepsData = healthDataArray
|
||||
.filter(data => data.type === 'steps' && data.steps != null)
|
||||
.filter((_, index) => index % frequency === 0)
|
||||
.slice(-maxPoints)
|
||||
const data = stepsData.map(item => item.steps!)
|
||||
const labels = stepsData.map((_, index) => ((index + 1) % 10 === 0 ? `${index + 1}` : ''))
|
||||
return { type: 'bar' as ChartType, data, labels, color }
|
||||
}
|
||||
|
||||
const intervalSeconds = Math.max(1, Math.floor(freq))
|
||||
const intervalMs = intervalSeconds * 1000
|
||||
const buckets = new Map<number, number[]>()
|
||||
for (let i = 0; i < healthDataArray.length; i++) {
|
||||
const d = healthDataArray[i]
|
||||
if (d.type !== 'steps' || d.steps == null) continue
|
||||
const ts = (d.timestamp == null) ? Date.now() : (typeof d.timestamp === 'number' ? d.timestamp : new Date(d.timestamp as string).getTime())
|
||||
const key = Math.floor(ts / intervalMs) * intervalMs
|
||||
if (!buckets.has(key)) buckets.set(key, [])
|
||||
const stepsValue = d.steps != null ? Number.parseFloat(d.steps.toString()) : 0
|
||||
buckets.get(key)!.push(stepsValue)
|
||||
}
|
||||
const keys: number[] = []
|
||||
buckets.forEach((_, key) => { keys.push(key) })
|
||||
keys.sort((a, b) => a - b)
|
||||
const slicedKeys = keys.slice(-maxPoints)
|
||||
|
||||
const data: number[] = []
|
||||
const labels: string[] = []
|
||||
for (let i = 0; i < slicedKeys.length; i++) {
|
||||
const key = slicedKeys[i]
|
||||
const bucketValue = buckets.get(key)
|
||||
const arr = (bucketValue != null) ? bucketValue : []
|
||||
let lastValue = NaN
|
||||
if (arr.length > 0) {
|
||||
const candidate = Number.parseFloat(arr[arr.length - 1].toString())
|
||||
lastValue = isNaN(candidate) ? NaN : candidate
|
||||
}
|
||||
data.push(lastValue)
|
||||
const labelDate = new Date(key)
|
||||
const ss = labelDate.getSeconds().toString().padStart(2, '0')
|
||||
const mm = labelDate.getMinutes().toString().padStart(2, '0')
|
||||
labels.push(`${mm}:${ss}`)
|
||||
}
|
||||
return { type: 'bar' as ChartType, data, labels, color }
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对齐的多指标图表数据(心率、血氧、速度、步数)
|
||||
* 返回一个数组,包含每个指标对应的 ChartOption,且 labels 保持一致
|
||||
*/
|
||||
export function processMultiMetricCharts(
|
||||
healthDataArray: HealthData[],
|
||||
intervalSeconds: number = 1,
|
||||
maxPoints: number = 100,
|
||||
metrics: ('heart' | 'spo2' | 'steps' | 'speed')[] = ['heart', 'spo2', 'steps']
|
||||
): ChartOption[] {
|
||||
const intervalMs = Math.max(1, intervalSeconds) * 1000
|
||||
const buckets = new Map<number, MultiMetricBucket>()
|
||||
|
||||
for (let i = 0; i < healthDataArray.length; i++) {
|
||||
const d = healthDataArray[i]
|
||||
const ts = (d.timestamp == null) ? Date.now() : (typeof d.timestamp === 'number' ? d.timestamp : new Date(d.timestamp).getTime())
|
||||
const key = Math.floor(ts / intervalMs) * intervalMs
|
||||
if (!buckets.has(key)) buckets.set(key, {})
|
||||
const entry = buckets.get(key)!
|
||||
if (d.type === 'heart' && d.heartRate != null) {
|
||||
if (!entry.heart) entry.heart = []
|
||||
const heartRateValue = d.heartRate != null ? Number.parseFloat(d.heartRate.toString()) : 0
|
||||
entry.heart.push(heartRateValue)
|
||||
}
|
||||
if (d.type === 'spo2' && d.spo2 != null) {
|
||||
if (!entry.spo2) entry.spo2 = []
|
||||
const spo2Value = d.spo2 != null ? Number.parseFloat(d.spo2.toString()) : 0
|
||||
entry.spo2.push(spo2Value)
|
||||
}
|
||||
if ((d.type === 'speed' || d.type === 'heart') && (d as any).speed != null) {
|
||||
if (!entry.speed) entry.speed = []
|
||||
const speedValue = (d as any).speed != null ? Number.parseFloat((d as any).speed.toString()) : 0
|
||||
entry.speed.push(speedValue)
|
||||
}
|
||||
if (d.type === 'steps' && d.steps != null) {
|
||||
if (!entry.steps) entry.steps = []
|
||||
const stepsValue = d.steps != null ? Number.parseFloat(d.steps.toString()) : 0
|
||||
entry.steps.push(stepsValue)
|
||||
}
|
||||
}
|
||||
|
||||
const keys: number[] = []
|
||||
buckets.forEach((_, key) => { keys.push(key) })
|
||||
keys.sort((a, b) => a - b)
|
||||
const slicedKeys = keys.slice(-maxPoints)
|
||||
const labels: string[] = []
|
||||
for (let i = 0; i < slicedKeys.length; i++) {
|
||||
const key = slicedKeys[i]
|
||||
const labelDate = new Date(key)
|
||||
const ss = labelDate.getSeconds().toString().padStart(2, '0')
|
||||
const mm = labelDate.getMinutes().toString().padStart(2, '0')
|
||||
labels.push(`${mm}:${ss}`)
|
||||
}
|
||||
|
||||
const results: ChartOption[] = []
|
||||
if (metrics.includes('heart')) {
|
||||
const data: number[] = []
|
||||
for (let i = 0; i < slicedKeys.length; i++) {
|
||||
const key = slicedKeys[i]
|
||||
const bucketValue = buckets.get(key)
|
||||
const arr = (bucketValue?.heart != null) ? bucketValue.heart : []
|
||||
let maxValue = NaN
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
const num = Number.parseFloat(arr[j].toString())
|
||||
if (isNaN(num)) continue
|
||||
if (isNaN(maxValue) || num > maxValue) {
|
||||
maxValue = num
|
||||
}
|
||||
}
|
||||
data.push(maxValue)
|
||||
}
|
||||
results.push({ type: 'line' as ChartType, data, labels, color: '#FF6B6B' })
|
||||
}
|
||||
if (metrics.includes('spo2')) {
|
||||
const data: number[] = []
|
||||
for (let i = 0; i < slicedKeys.length; i++) {
|
||||
const key = slicedKeys[i]
|
||||
const bucketValue = buckets.get(key)
|
||||
const arr = (bucketValue?.spo2 != null) ? bucketValue.spo2 : []
|
||||
let maxValue = NaN
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
const num = Number(arr[j])
|
||||
if (isNaN(num)) continue
|
||||
if (isNaN(maxValue) || num > maxValue) {
|
||||
maxValue = num
|
||||
}
|
||||
}
|
||||
data.push(maxValue)
|
||||
}
|
||||
results.push({ type: 'line' as ChartType, data, labels, color: '#2196F3' })
|
||||
}
|
||||
if (metrics.includes('speed')) {
|
||||
const data: number[] = []
|
||||
for (let i = 0; i < slicedKeys.length; i++) {
|
||||
const key = slicedKeys[i]
|
||||
const bucketValue = buckets.get(key)
|
||||
const arr = (bucketValue?.speed != null) ? bucketValue.speed : []
|
||||
let maxValue = NaN
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
const num = Number(arr[j])
|
||||
if (isNaN(num)) continue
|
||||
if (isNaN(maxValue) || num > maxValue) {
|
||||
maxValue = num
|
||||
}
|
||||
}
|
||||
data.push(maxValue)
|
||||
}
|
||||
results.push({ type: 'line' as ChartType, data, labels, color: '#FFA726' })
|
||||
}
|
||||
if (metrics.includes('steps')) {
|
||||
const data: number[] = []
|
||||
for (let i = 0; i < slicedKeys.length; i++) {
|
||||
const key = slicedKeys[i]
|
||||
const bucketValue = buckets.get(key)
|
||||
const arr = (bucketValue?.steps != null) ? bucketValue.steps : []
|
||||
let lastValue = NaN
|
||||
if (arr.length > 0) {
|
||||
const candidate = Number(arr[arr.length - 1])
|
||||
lastValue = isNaN(candidate) ? NaN : candidate
|
||||
}
|
||||
data.push(lastValue)
|
||||
}
|
||||
results.push({ type: 'bar' as ChartType, data, labels, color: '#4CAF50' })
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建多指标组合图表数据
|
||||
* @param healthDataArray 健康数据数组
|
||||
* @param metrics 要包含的指标数组
|
||||
* @param maxPoints 最大显示点数
|
||||
* @returns ChartOption对象数组,每个指标一个图表
|
||||
*/
|
||||
export function createMultiMetricCharts(
|
||||
healthDataArray: HealthData[],
|
||||
metrics: ('heart' | 'spo2' | 'steps')[] = ['heart', 'spo2', 'steps'],
|
||||
maxPoints: number = 100
|
||||
): ChartOption[] {
|
||||
const charts: ChartOption[] = []
|
||||
|
||||
if (metrics.includes('heart')) {
|
||||
charts.push(processHeartRateData(healthDataArray, { maxPoints }))
|
||||
}
|
||||
|
||||
if (metrics.includes('spo2')) {
|
||||
charts.push(processSpo2Data(healthDataArray, { maxPoints }))
|
||||
}
|
||||
|
||||
if (metrics.includes('steps')) {
|
||||
charts.push(processStepsData(healthDataArray, { maxPoints }))
|
||||
}
|
||||
|
||||
return charts
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据的最新值
|
||||
* @param healthDataArray 健康数据数组
|
||||
* @param dataType 数据类型
|
||||
* @returns 最新值或null
|
||||
*/
|
||||
export function getLatestValue(
|
||||
healthDataArray: HealthData[],
|
||||
dataType: 'heart' | 'spo2' | 'steps'
|
||||
): number | null {
|
||||
const filteredData = healthDataArray
|
||||
.filter(data => data.type === dataType)
|
||||
.filter(data => {
|
||||
switch (dataType) {
|
||||
case 'heart': return data.heartRate != null
|
||||
case 'spo2': return data.spo2 != null
|
||||
case 'steps': return data.steps != null
|
||||
default: return false
|
||||
}
|
||||
})
|
||||
|
||||
if (filteredData.length === 0) return null
|
||||
|
||||
const latest = filteredData[filteredData.length - 1]
|
||||
switch (dataType) {
|
||||
case 'heart': return latest.heartRate!
|
||||
case 'spo2': return latest.spo2!
|
||||
case 'steps': return latest.steps!
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有足够的数据用于图表显示
|
||||
* @param healthDataArray 健康数据数组
|
||||
* @param dataType 数据类型
|
||||
* @param minPoints 最少点数,默认3
|
||||
* @returns 是否有足够数据
|
||||
*/
|
||||
export function hasEnoughDataForChart(
|
||||
healthDataArray: HealthData[],
|
||||
dataType: 'heart' | 'spo2' | 'steps',
|
||||
minPoints: number = 3
|
||||
): boolean {
|
||||
const filteredData = healthDataArray.filter(data => data.type === dataType)
|
||||
return filteredData.length >= minPoints
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试数据处理功能(用于开发调试)
|
||||
* @returns 测试结果
|
||||
*/
|
||||
export function testChartDataProcessing(): string {
|
||||
// 创建测试数据
|
||||
const testData: HealthData[] = [
|
||||
{ type: 'heart', heartRate: 72, timestamp: Date.now() },
|
||||
{ type: 'spo2', spo2: 98, timestamp: Date.now() },
|
||||
{ type: 'steps', steps: 150, timestamp: Date.now() },
|
||||
{ type: 'heart', heartRate: 75, timestamp: Date.now() },
|
||||
{ type: 'spo2', spo2: 97, timestamp: Date.now() },
|
||||
{ type: 'steps', steps: 200, timestamp: Date.now() }
|
||||
]
|
||||
|
||||
// 测试心率数据处理
|
||||
const heartChart = processHeartRateData(testData, { maxPoints: 100 })
|
||||
const spo2Chart = processSpo2Data(testData, { maxPoints: 100 })
|
||||
const stepsChart = processStepsData(testData, { maxPoints: 100 })
|
||||
|
||||
return `测试完成 - 心率: ${heartChart.data.length}点, 血氧: ${spo2Chart.data.length}点, 步数: ${stepsChart.data.length}点`
|
||||
}
|
||||
261
uts/utils/dfu.uts
Normal file
261
uts/utils/dfu.uts
Normal file
@@ -0,0 +1,261 @@
|
||||
// DFU (Device Firmware Update) 工具类
|
||||
// 用于处理设备固件升级过程
|
||||
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
export type FirmwareInfo {
|
||||
version: string
|
||||
url: string
|
||||
size: number
|
||||
checksum: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
//https://ak3.oulog.com/storage/v1/object/public/akmon/OmFw2510221553.zip
|
||||
export type FirmwareConfig {
|
||||
default_value: string
|
||||
default_key?: string
|
||||
}
|
||||
|
||||
export type DFUProgress {
|
||||
stage: 'downloading' | 'verifying' | 'uploading' | 'upgrading' | 'completed'
|
||||
progress: number // 0-100
|
||||
message: string
|
||||
}
|
||||
|
||||
export class DFUTool {
|
||||
private progressCallback?: (progress: DFUProgress) => void
|
||||
|
||||
constructor(progressCallback?: (progress: DFUProgress) => void) {
|
||||
this.progressCallback = progressCallback
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有可用的固件升级
|
||||
* @param currentHardwareVersion 当前硬件版本
|
||||
* @param currentSoftwareVersion 当前软件版本
|
||||
* @returns 固件信息或null
|
||||
*/
|
||||
async checkFirmwareUpdate(currentHardwareVersion: string, currentSoftwareVersion: string): Promise<FirmwareInfo | null> {
|
||||
try {
|
||||
// 从Supabase查询最新固件版本
|
||||
const result = await supa
|
||||
.from('ak_global_config')
|
||||
.select('default_value,default_key', {})
|
||||
.eq('config_key', 'cfwatch_hardware_version')
|
||||
.single()
|
||||
.executeAs<FirmwareConfig>()
|
||||
console.log(result.data)
|
||||
const data = result.data as FirmwareConfig
|
||||
if (result.error !=null ) {
|
||||
console.log('查询固件版本失败:', result.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const firmwareConfig = data
|
||||
|
||||
// 比较版本号,检查是否需要升级
|
||||
if (this.compareVersions(currentSoftwareVersion, firmwareConfig.default_value) < 0) {
|
||||
return {
|
||||
version: firmwareConfig.default_value,
|
||||
url: firmwareConfig.default_key,
|
||||
size: 0,
|
||||
checksum: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('检查固件升级失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行固件升级
|
||||
* @param firmwareInfo 固件信息
|
||||
* @param deviceId 设备ID
|
||||
* @returns 升级结果
|
||||
*/
|
||||
async performFirmwareUpdate(firmwareInfo: FirmwareInfo, deviceId: string): Promise<boolean> {
|
||||
try {
|
||||
this.updateProgress('downloading', 0, '开始下载固件...')
|
||||
|
||||
// 下载固件文件
|
||||
const firmwareData = await this.downloadFirmware(firmwareInfo.url)
|
||||
|
||||
this.updateProgress('verifying', 50, '验证固件完整性...')
|
||||
|
||||
// 验证文件完整性
|
||||
if (!this.verifyFirmware(firmwareData, firmwareInfo.checksum)) {
|
||||
throw new Error('固件校验失败')
|
||||
}
|
||||
|
||||
this.updateProgress('uploading', 70, '上传固件到设备...')
|
||||
|
||||
// 执行DFU升级
|
||||
const success = await this.executeDFU(firmwareData, deviceId)
|
||||
|
||||
if (success) {
|
||||
this.updateProgress('completed', 100, '固件升级完成')
|
||||
return true
|
||||
} else {
|
||||
throw new Error('DFU升级失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('固件升级失败:', error)
|
||||
this.updateProgress('completed', 0, `升级失败: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载固件文件
|
||||
*/
|
||||
private async downloadFirmware(url: string): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.downloadFile({
|
||||
url: url,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
// 读取下载的文件
|
||||
uni.getFileSystemManager().readFile({
|
||||
filePath: res.tempFilePath,
|
||||
success: (readRes) => {
|
||||
resolve(readRes.data as ArrayBuffer)
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
} else {
|
||||
reject(new Error(`下载失败: ${res.statusCode}`))
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证固件完整性
|
||||
*/
|
||||
private verifyFirmware(data: ArrayBuffer, expectedChecksum: string): boolean {
|
||||
// 简单的校验和验证(可以根据需要实现更复杂的校验)
|
||||
const hash = this.calculateSimpleHash(data)
|
||||
return hash === expectedChecksum
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行DFU升级
|
||||
*/
|
||||
private async executeDFU(firmwareData: ArrayBuffer, deviceId: string): Promise<boolean> {
|
||||
// 这里实现具体的DFU协议
|
||||
// 需要根据设备的DFU协议来实现
|
||||
// 这是一个简化的实现,实际需要根据具体设备协议调整
|
||||
|
||||
try {
|
||||
// 1. 进入DFU模式
|
||||
await this.enterDFUMode(deviceId)
|
||||
|
||||
// 2. 发送固件数据包
|
||||
await this.sendFirmwarePackets(firmwareData, deviceId)
|
||||
|
||||
// 3. 验证升级结果
|
||||
await this.verifyUpdate(deviceId)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('DFU执行失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入DFU模式
|
||||
*/
|
||||
private async enterDFUMode(deviceId: string): Promise<void> {
|
||||
// 实现进入DFU模式的逻辑
|
||||
// 通常需要发送特定的命令到设备
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送固件数据包
|
||||
*/
|
||||
private async sendFirmwarePackets(firmwareData: ArrayBuffer, deviceId: string): Promise<void> {
|
||||
// 实现分包发送固件的逻辑
|
||||
const packetSize = 20 // 根据设备协议调整
|
||||
const totalPackets = Math.ceil(firmwareData.byteLength / packetSize)
|
||||
|
||||
for (let i = 0; i < totalPackets; i++) {
|
||||
const start = i * packetSize
|
||||
const end = Math.min(start + packetSize, firmwareData.byteLength)
|
||||
const packet = firmwareData.slice(start, end)
|
||||
|
||||
// 发送数据包
|
||||
await this.sendPacket(packet, deviceId)
|
||||
|
||||
// 更新进度
|
||||
const progress = Math.round((i + 1) / totalPackets * 100)
|
||||
this.updateProgress('uploading', 70 + (progress * 0.2), `上传进度: ${i + 1}/${totalPackets}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送单个数据包
|
||||
*/
|
||||
private async sendPacket(packet: ArrayBuffer, deviceId: string): Promise<void> {
|
||||
// 实现BLE发送数据包的逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证升级结果
|
||||
*/
|
||||
private async verifyUpdate(deviceId: string): Promise<void> {
|
||||
// 实现验证升级结果的逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算简单校验和
|
||||
*/
|
||||
private calculateSimpleHash(data: ArrayBuffer): string {
|
||||
const view = new Uint8Array(data)
|
||||
let hash = 0
|
||||
for (let i = 0; i < view.length; i++) {
|
||||
hash = ((hash << 5) - hash + view[i]) & 0xffffffff
|
||||
}
|
||||
return hash.toString(16)
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
*/
|
||||
private compareVersions(version1: string, version2: string): number {
|
||||
const v1Parts = version1.split('.').map((item) => parseInt(item))
|
||||
const v2Parts = version2.split('.').map((item) => parseInt(item))
|
||||
|
||||
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
||||
const v1 = v1Parts[i] != null ? v1Parts[i] : 0
|
||||
const v2 = v2Parts[i] != null ? v2Parts[i] : 0
|
||||
|
||||
if (v1 > v2) return 1
|
||||
if (v1 < v2) return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新进度
|
||||
*/
|
||||
private updateProgress(stage: 'downloading' | 'verifying' | 'uploading' | 'upgrading' | 'completed', progress: number, message: string): void {
|
||||
if (this.progressCallback != null) {
|
||||
this.progressCallback({
|
||||
stage,
|
||||
progress: Math.min(100, Math.max(0, progress)),
|
||||
message
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const dfuTool = new DFUTool(null)
|
||||
Reference in New Issue
Block a user