Files
WebTools/src/tools/CrcTool.vue
T

223 lines
10 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 } from '@lucide/vue'
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
import FileDropField from '@/components/FileDropField.vue'
import ToolPage from '@/components/ToolPage.vue'
import { useTransactionalFileWorker } from '@/composables/useTransactionalFileWorker'
import { copyText } from '@/lib/browser'
import { CRC_PRESETS, type CrcParameters } from '@/lib/crc'
import type { CrcBlockResult, CrcWorkerCalculateRequest, 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 firmware = shallowRef<FirmwareData>()
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 error = ref('')
const success = ref('')
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() {
invalidateCalculation()
combinedResult.value = ''
blockResults.value = []
success.value = ''
}
function invalidateResults() {
clearResults()
error.value = ''
}
const {
parsing,
calculating,
parseFile,
calculate: calculateInWorker,
invalidateCalculation,
destroyWorkers,
} = useTransactionalFileWorker<Omit<CrcWorkerCalculateRequest, 'type' | 'id'>, CrcWorkerResponse>({
createWorker: () => new Worker(new URL('../workers/crc.worker.ts', import.meta.url), { type: 'module' }),
onParsed: (response, selected) => {
clearResults()
firmware.value = {
fileName: selected.name,
fileSize: selected.size,
format: response.format,
totalBytes: response.totalBytes,
blockCount: response.blockCount,
warnings: response.warnings,
}
success.value = `已解析 ${response.blockCount} 个数据块`
},
onCalculated: (response) => {
combinedResult.value = response.combinedResult
blockResults.value = response.blockResults
success.value = `${activeCalculationName} 计算完成`
},
onParseError: (message) => { error.value = message },
onCalculationError: (message) => { error.value = message },
onPendingWorkerError: (hasCurrentFile) => {
error.value = hasCurrentFile
? '新文件解析失败,当前文件未更改'
: 'CRC 后台线程异常终止,请重新选择文件'
},
onActiveWorkerError: () => {
firmware.value = undefined
clearResults()
error.value = 'CRC 后台线程异常终止,请重新选择文件'
},
})
let activeCalculationName = ''
function selectFile(selected: File) {
if (selected.size === 0) { error.value = '文件为空'; return }
if (selected.size > MAX_FIRMWARE_FILE_SIZE) { error.value = '文件不能超过 64 MiB'; return }
if (!/\.(?:hex|ihx|bin)$/i.test(selected.name)) { error.value = '请选择 .hex、.ihx 或 .bin 文件'; return }
error.value = ''
success.value = ''
parseFile(selected)
}
function calculate() {
invalidateResults()
if (!firmware.value) { error.value = '请先选择 BIN 或 Intel HEX 文件'; return }
try {
const parameters = getParameters()
activeCalculationName = activeParameterName.value
const startStatus = calculateInWorker({ parameters })
if (startStatus === 'unavailable') throw new Error('文件数据已失效,请重新选择文件')
} catch (reason) {
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() {
destroyWorkers()
invalidateResults()
firmware.value = undefined
}
onBeforeUnmount(destroyWorkers)
</script>
<template>
<ToolPage category="校验计算" title="CRC" description="解析 BIN 或 Intel HEX 有效数据,并使用预设或自定义参数计算 CRC。">
<FileDropField
input-id="crc-file"
replacement-input-id="crc-replacement-file"
accept=".hex,.ihx,.bin,application/octet-stream"
:has-file="Boolean(firmware)"
:busy="parsing"
initial-label="选择或拖入 HEX / BIN 文件"
initial-busy-label="正在解析文件…"
hint="Intel HEX 会校验记录并识别多个数据块"
compact
@select="selectFile"
@clear="clearFile"
>
<template #icon><FileUp :size="28" aria-hidden="true" /></template>
<template v-if="firmware"><strong>{{ firmware.fileName }}</strong><span>{{ firmware.format }} · {{ firmware.totalBytes.toLocaleString() }} 有效字节 · {{ firmware.blockCount }} 个 block</span></template>
</FileDropField>
<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>