yunsi-toolbox-vue/components/app-image-viewer/AppImageViewer.vue

158 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, watch } from 'vue';
import {
type AppImageInfo,
SPI_FLASH_MODE_NAMES, SPI_FLASH_SPEED_NAMES, SPI_FLASH_SIZE_NAMES,
parseAppImage,
} from '../../lib/app-image';
const props = defineProps<{
isDark?: boolean;
}>();
const imageInfo = ref<AppImageInfo | null>(null);
const statusMessage = ref('');
const statusType = ref<'success' | 'error' | 'info'>('info');
const fileName = ref('');
let statusTimer: ReturnType<typeof setTimeout> | null = null;
function showStatus(msg: string, type: 'success' | 'error' | 'info' = 'info') {
if (statusTimer !== null) {
clearTimeout(statusTimer);
statusTimer = null;
}
statusMessage.value = msg;
statusType.value = type;
statusTimer = setTimeout(() => {
statusMessage.value = '';
statusTimer = null;
}, 4000);
}
function formatHex(val: number): string {
return '0x' + val.toString(16).toUpperCase();
}
function formatHexDump(data: Uint8Array): string {
return Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' ');
}
function formatSha256(data: Uint8Array): string {
// Check if all zeros (not computed)
if (data.every(b => b === 0)) return '(未计算)';
return Array.from(data).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function handleOpenFile(file: File): Promise<false> {
try {
const buffer = await file.arrayBuffer();
const data = new Uint8Array(buffer);
if (data.length >= 4 &&
data[0] === 0x7F && data[1] === 0x45 && data[2] === 0x4C && data[3] === 0x46) {
throw new Error('ELF 格式不支持。请使用 esptool.py elf2image 将其转换为 .bin 文件');
}
imageInfo.value = parseAppImage(data);
fileName.value = file.name;
showStatus(`已加载 ${file.name} (${data.byteLength} 字节)`, 'success');
} catch (e: any) {
imageInfo.value = null;
showStatus(`加载失败: ${e.message}`, 'error');
}
return false;
}
</script>
<template>
<div>
<!-- Status message -->
<transition name="el-fade-in">
<el-alert
v-if="statusMessage"
:title="statusMessage"
:type="statusType"
show-icon
closable
class="mb-3"
@close="statusMessage = ''"
/>
</transition>
<!-- Upload -->
<div class="mb-4">
<el-upload :before-upload="handleOpenFile" :show-file-list="false" accept=".bin">
<el-button type="primary">打开固件文件</el-button>
</el-upload>
</div>
<template v-if="imageInfo">
<!-- App Description -->
<template v-if="imageInfo.appDescription">
<el-text tag="b" class="block mb-2">应用信息</el-text>
<el-descriptions :column="2" border size="small" class="mb-4">
<el-descriptions-item label="项目名称">{{ imageInfo.appDescription.projectName }}</el-descriptions-item>
<el-descriptions-item label="版本">{{ imageInfo.appDescription.version }}</el-descriptions-item>
<el-descriptions-item label="IDF版本">{{ imageInfo.appDescription.idfVersion }}</el-descriptions-item>
<el-descriptions-item label="安全版本">{{ imageInfo.appDescription.secureVersion }}</el-descriptions-item>
<el-descriptions-item label="编译日期">{{ imageInfo.appDescription.compileDate }}</el-descriptions-item>
<el-descriptions-item label="编译时间">{{ imageInfo.appDescription.compileTime }}</el-descriptions-item>
<el-descriptions-item label="ELF SHA256" :span="2">
<el-text size="small" class="font-mono break-all">
{{ formatSha256(imageInfo.appDescription.appElfSha256) }}
</el-text>
</el-descriptions-item>
</el-descriptions>
</template>
<!-- Image Header -->
<el-text tag="b" class="block mb-2">镜像头</el-text>
<el-descriptions :column="2" border size="small" class="mb-4">
<el-descriptions-item label="芯片">{{ imageInfo.chipName }}</el-descriptions-item>
<el-descriptions-item label="入口地址">{{ formatHex(imageInfo.header.entryPoint) }}</el-descriptions-item>
<el-descriptions-item label="SPI模式">{{ SPI_FLASH_MODE_NAMES[imageInfo.header.spiMode] ?? formatHex(imageInfo.header.spiMode) }}</el-descriptions-item>
<el-descriptions-item label="SPI速度">{{ SPI_FLASH_SPEED_NAMES[imageInfo.header.spiSpeed] ?? formatHex(imageInfo.header.spiSpeed) }}</el-descriptions-item>
<el-descriptions-item label="Flash大小">{{ SPI_FLASH_SIZE_NAMES[imageInfo.header.spiSize] ?? formatHex(imageInfo.header.spiSize) }}</el-descriptions-item>
<el-descriptions-item label="段数">{{ imageInfo.header.segmentCount }}</el-descriptions-item>
<el-descriptions-item label="WP引脚">{{ formatHex(imageInfo.extendedHeader.wpPin) }}</el-descriptions-item>
<el-descriptions-item label="SPI引脚驱动">{{ imageInfo.extendedHeader.spiPinDrv.map(formatHex).join(' / ') }}</el-descriptions-item>
<el-descriptions-item label="最小芯片版本">{{ imageInfo.extendedHeader.minChipRevFull / 100 }}</el-descriptions-item>
<el-descriptions-item label="最大芯片版本">{{ imageInfo.extendedHeader.maxChipRevFull === 0xFFFF ? '不限' : imageInfo.extendedHeader.maxChipRevFull / 100 }}</el-descriptions-item>
<el-descriptions-item label="附加哈希">{{ imageInfo.extendedHeader.hashAppended ? '是' : '否' }}</el-descriptions-item>
</el-descriptions>
<!-- Segments -->
<el-text tag="b" class="block mb-2">段列表</el-text>
<el-table :data="imageInfo.segments" border stripe size="small" max-height="300">
<el-table-column label="#" width="50" type="index" />
<el-table-column label="加载地址" width="160">
<template #default="{ row }">{{ formatHex(row.loadAddr) }}</template>
</el-table-column>
<el-table-column label="数据大小">
<template #default="{ row }">
{{ row.dataLen }} 字节 ({{ (row.dataLen / 1024).toFixed(1) }} KB)
</template>
</el-table-column>
</el-table>
<!-- Custom App Description (raw bytes) -->
<template v-if="imageInfo.customDescRawBytes && !imageInfo.customDescRawBytes.every(b => b === 0)">
<el-text tag="b" class="block mb-2">自定义应用描述(偏移 288 B原始字节</el-text>
<el-text size="small" class="font-mono break-all">
{{ formatHexDump(imageInfo.customDescRawBytes) }}
</el-text>
</template>
</template>
<el-empty v-else description="请打开一个ESP32固件文件 (.bin)" />
</div>
</template>
<style scoped>
.font-mono {
font-family: 'Courier New', Courier, monospace;
}
.break-all {
word-break: break-all;
}
</style>