Files
WebTools/src/tools/CrcTool.vue
T

272 lines
12 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 { AlertCircle, Calculator, CheckCircle2, Copy, FileUp, Trash2, Upload } from '@lucide/vue'
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
import ToolPage from '@/components/ToolPage.vue'
import { copyText } from '@/lib/browser'
import { CRC_PRESETS, type CrcParameters } from '@/lib/crc'
import type { CrcBlockResult, CrcWorkerRequest, CrcWorkerResponse } from '@/lib/crcWorker'
import { MAX_FIRMWARE_FILE_SIZE, type FirmwareFormat } from '@/lib/firmware'
interface FirmwareData {
fileName: string
fileSize: number
format: FirmwareFormat
blockCount: number
totalBytes: number
warnings: string[]
}
const defaultPreset = CRC_PRESETS[0]
const fileInput = ref<HTMLInputElement>()
const firmware = shallowRef<FirmwareData>()
const dragging = ref(false)
const selectedPreset = ref(defaultPreset.id)
const width = ref(defaultPreset.width)
const polynomial = ref(defaultPreset.polynomial.toString(16).toUpperCase())
const initialValue = ref(defaultPreset.initialValue.toString(16).toUpperCase())
const xorOut = ref(defaultPreset.xorOut.toString(16).toUpperCase())
const inputReflected = ref(defaultPreset.inputReflected)
const outputReflected = ref(defaultPreset.outputReflected)
const combinedResult = ref('')
const blockResults = ref<CrcBlockResult[]>([])
const parsing = ref(false)
const calculating = ref(false)
const error = ref('')
const success = ref('')
let worker: Worker | undefined
let requestSequence = 0
let activeParseRequest: number | undefined
let activeCalculationRequest: number | undefined
const activeParameterName = computed(() => CRC_PRESETS.find((preset) => preset.id === selectedPreset.value)?.name ?? '自定义参数')
function formatAddress(address: number): string {
return `0x${address.toString(16).toUpperCase().padStart(8, '0')}`
}
function applyPreset() {
const preset = CRC_PRESETS.find((item) => item.id === selectedPreset.value)
if (!preset) return
width.value = preset.width
polynomial.value = preset.polynomial.toString(16).toUpperCase()
initialValue.value = preset.initialValue.toString(16).toUpperCase()
xorOut.value = preset.xorOut.toString(16).toUpperCase()
inputReflected.value = preset.inputReflected
outputReflected.value = preset.outputReflected
invalidateResults()
}
function markCustom() {
selectedPreset.value = 'custom'
invalidateResults()
}
function parseParameter(value: string, label: string): bigint {
const normalized = value.trim().replace(/^0x/i, '')
if (!normalized || !/^[0-9a-f]+$/i.test(normalized)) throw new Error(`${label} 必须是十六进制整数`)
return BigInt(`0x${normalized}`)
}
function getParameters(): CrcParameters {
return {
width: width.value,
polynomial: parseParameter(polynomial.value, '多项式(Poly)'),
initialValue: parseParameter(initialValue.value, '初始值(Init)'),
inputReflected: inputReflected.value,
outputReflected: outputReflected.value,
xorOut: parseParameter(xorOut.value, '异或输出(XorOut)'),
}
}
function clearResults() {
combinedResult.value = ''
blockResults.value = []
success.value = ''
}
function invalidateResults() {
activeCalculationRequest = undefined
calculating.value = false
clearResults()
error.value = ''
}
function destroyWorker() {
worker?.terminate()
worker = undefined
activeParseRequest = undefined
activeCalculationRequest = undefined
parsing.value = false
calculating.value = false
}
function createWorker(): Worker {
const nextWorker = new Worker(new URL('../workers/crc.worker.ts', import.meta.url), { type: 'module' })
nextWorker.onmessage = (event: MessageEvent<CrcWorkerResponse>) => {
if (nextWorker !== worker) return
const response = event.data
if (response.type === 'parsed') {
if (response.id !== activeParseRequest) return
activeParseRequest = undefined
parsing.value = false
firmware.value = {
fileName: pendingFileName,
fileSize: pendingFileSize,
format: response.format,
totalBytes: response.totalBytes,
blockCount: response.blockCount,
warnings: response.warnings,
}
success.value = `已解析 ${response.blockCount} 个数据块`
return
}
if (response.type === 'calculated') {
if (response.id !== activeCalculationRequest) return
activeCalculationRequest = undefined
calculating.value = false
combinedResult.value = response.combinedResult
blockResults.value = response.blockResults
success.value = `${activeCalculationName} 计算完成`
return
}
if (response.operation === 'parse' && response.id === activeParseRequest) {
activeParseRequest = undefined
parsing.value = false
firmware.value = undefined
error.value = response.error
} else if (response.operation === 'calculate' && response.id === activeCalculationRequest) {
activeCalculationRequest = undefined
calculating.value = false
error.value = response.error
}
}
nextWorker.onerror = () => {
if (nextWorker !== worker) return
destroyWorker()
firmware.value = undefined
clearResults()
error.value = 'CRC 后台线程异常终止,请重新选择文件'
}
worker = nextWorker
return nextWorker
}
let pendingFileName = ''
let pendingFileSize = 0
let activeCalculationName = ''
function selectFile(selected?: File) {
if (!selected) return
destroyWorker()
firmware.value = undefined
clearResults()
error.value = ''
if (selected.size === 0) { error.value = '文件为空'; return }
if (selected.size > MAX_FIRMWARE_FILE_SIZE) { error.value = '文件不能超过 64 MiB'; return }
pendingFileName = selected.name
pendingFileSize = selected.size
const id = ++requestSequence
activeParseRequest = id
parsing.value = true
const request: CrcWorkerRequest = { type: 'parse', id, file: selected }
createWorker().postMessage(request)
}
function onFileInput(event: Event) {
const input = event.target as HTMLInputElement
const selected = input.files?.[0]
input.value = ''
selectFile(selected)
}
function onDrop(event: DragEvent) {
dragging.value = false
void selectFile(event.dataTransfer?.files[0])
}
function calculate() {
invalidateResults()
if (!firmware.value) { error.value = '请先选择 BIN 或 Intel HEX 文件'; return }
try {
const parameters = getParameters()
if (!worker) throw new Error('文件数据已失效,请重新选择文件')
const id = ++requestSequence
activeCalculationRequest = id
activeCalculationName = activeParameterName.value
calculating.value = true
const request: CrcWorkerRequest = { type: 'calculate', id, parameters }
worker.postMessage(request)
} catch (reason) {
activeCalculationRequest = undefined
calculating.value = false
error.value = reason instanceof Error ? reason.message : 'CRC 计算失败'
}
}
async function copyResult() {
try { await copyText(combinedResult.value); success.value = '总 CRC 已复制'; error.value = '' }
catch { error.value = '复制失败' }
}
function clearFile() {
destroyWorker()
invalidateResults()
firmware.value = undefined
if (fileInput.value) fileInput.value.value = ''
}
onBeforeUnmount(destroyWorker)
</script>
<template>
<ToolPage category="校验计算" title="CRC" description="解析 BIN 或 Intel HEX 有效数据,并使用预设或自定义参数计算 CRC。">
<label v-if="!firmware" class="upload-zone compact-upload" :class="{ 'is-dragging': dragging }" @dragover.prevent="dragging = true" @dragleave.prevent="dragging = false" @drop.prevent="onDrop">
<input id="crc-file" name="crc-file" ref="fileInput" type="file" accept=".hex,.ihx,.bin,application/octet-stream" :disabled="parsing" @change="onFileInput" />
<span><FileUp :size="28" aria-hidden="true" /><strong>{{ parsing ? '正在解析文件…' : '选择或拖入 HEX / BIN 文件' }}</strong><span>Intel HEX 会校验记录并识别多个数据块</span></span>
</label>
<div v-else class="firmware-summary">
<div><strong>{{ firmware.fileName }}</strong><span>{{ firmware.format }} · {{ firmware.totalBytes.toLocaleString() }} 有效字节 · {{ firmware.blockCount }} 个 block</span></div>
<label class="action-button"><Upload :size="15" />更换文件<input id="crc-replacement-file" name="crc-replacement-file" class="sr-only-file" type="file" accept=".hex,.ihx,.bin,application/octet-stream" @change="onFileInput" /></label>
<button class="action-button" type="button" @click="clearFile"><Trash2 :size="15" />清空</button>
</div>
<div class="crc-config">
<label class="parameter-field preset-field"><span>参数预设(Preset)</span>
<select id="crc-preset" name="crc-preset" v-model="selectedPreset" class="select-input" :disabled="calculating" @change="applyPreset">
<option v-for="preset in CRC_PRESETS" :key="preset.id" :value="preset.id">{{ preset.name }}</option>
<option value="custom">自定义参数</option>
</select>
</label>
<label class="parameter-field"><span>位宽(Width)</span>
<select id="crc-width" name="crc-width" v-model.number="width" class="select-input" :disabled="calculating" @change="markCustom"><option :value="8">8</option><option :value="16">16</option><option :value="32">32</option><option :value="64">64</option></select>
</label>
<label class="parameter-field"><span>多项式(Poly,Hex)</span><input id="crc-polynomial" name="crc-polynomial" v-model="polynomial" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label>
<label class="parameter-field"><span>初始值(Init,Hex)</span><input id="crc-initial-value" name="crc-initial-value" v-model="initialValue" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label>
<label class="parameter-field"><span>异或输出(XorOut,Hex)</span><input id="crc-xor-out" name="crc-xor-out" v-model="xorOut" class="text-input" :disabled="calculating" spellcheck="false" @input="markCustom" /></label>
<div class="crc-reflection-options" role="group" aria-label="位反转选项">
<label class="check-field crc-check"><input id="crc-ref-in" name="crc-ref-in" v-model="inputReflected" type="checkbox" :disabled="calculating" @change="markCustom" />输入位反转(RefIn)</label>
<label class="check-field crc-check"><input id="crc-ref-out" name="crc-ref-out" v-model="outputReflected" type="checkbox" :disabled="calculating" @change="markCustom" />输出位反转(RefOut)</label>
</div>
</div>
<div class="action-row">
<button class="action-button primary" type="button" :disabled="!firmware || parsing || calculating" @click="calculate"><Calculator :size="15" />{{ calculating ? '正在计算…' : '计算 CRC' }}</button>
<span class="action-spacer" />
<button class="action-button ghost" type="button" :disabled="!combinedResult" @click="copyResult"><Copy :size="15" />复制总 CRC</button>
</div>
<div v-if="combinedResult" class="crc-results">
<div class="crc-total"><span>总有效数据 · 按地址升序拼接,不填充空洞</span><strong>{{ combinedResult }}</strong></div>
<div class="result-table-wrap">
<table class="result-table">
<thead><tr><th>数据块(Block)</th><th>起始地址(Start Address)</th><th>结束地址(End Address)</th><th>字节数(Length)</th><th>CRC</th></tr></thead>
<tbody><tr v-for="(block, index) in blockResults" :key="block.startAddress"><td>#{{ index + 1 }}</td><td>{{ formatAddress(block.startAddress) }}</td><td>{{ formatAddress(block.endAddress) }}</td><td>{{ block.length.toLocaleString() }}</td><td class="crc-value">{{ block.crc }}</td></tr></tbody>
</table>
</div>
</div>
<div v-if="firmware?.warnings.length" class="status-message warning"><AlertCircle :size="16" />{{ firmware.warnings.join(';') }}</div>
<div v-if="error" class="status-message error"><AlertCircle :size="16" />{{ error }}</div>
<div v-else-if="success" class="status-message success"><CheckCircle2 :size="16" />{{ success }}</div>
</ToolPage>
</template>