yunsi-toolbox-vue/esp-flasher/EspFlasher.vue

482 lines
13 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 lang="ts">
interface Navigator {
serial: {
// Define the methods and properties you need from the Web Serial API
// For example:
requestPort: (options?: SerialPortRequestOptions) => Promise<SerialPort>;
getPorts: () => Promise<SerialPort[]>;
// Add other properties and methods as needed
};
}
</script>
<script setup lang="ts">
import 'xterm/css/xterm.css';
import {onBeforeMount, onMounted, reactive, ref, watch} from "vue";
import {ESPLoader, type FlashOptions, type IEspLoaderTerminal, type LoaderOptions, Transport} from "./lib_esptools-js";
import CryptoJS from "crypto-js";
import {useData} from 'vitepress';
const terminalContainer = ref();
let terminal: any;
let fitAddon: any;
const isDarkMode = useData().isDark;
watch(isDarkMode, value => {
});
const terminalConfig = {
theme: {
background: '#4b4b4b', // dark gray background
foreground: '#c5c8c6', // light gray text
cursor: '#f0c674', // yellow cursor
// You can also set specific ANSI colors if needed
black: '#1d1f21',
red: '#cc6666',
convertEol: true,
// ...and so on for other colors
}
}
const notSupportedMsg = "您的浏览器不支持虚拟串口请使用电脑版Chrome或者Edge。"
onBeforeMount(() => {
if (!('serial' in navigator)) {
alert(notSupportedMsg);
console.log("Serial not supported");
} else {
console.log("serial ok");
serialSupported.value = true;
}
});
onMounted(async () => {
if ('serial' in navigator) {
const { Terminal } = await import('xterm');
const { FitAddon } = await import('xterm-addon-fit');
fitAddon = new FitAddon();
terminal = new Terminal(terminalConfig);
terminal.loadAddon(fitAddon);
// Initialize the terminal
// terminal.open(terminalContainer.value);
terminal.open(terminalContainer.value);
fitAddon.fit();
// You can write some data to the terminal or set up your own handlers here
terminal.writeln('请选择一个固件,连接后烧录。');
const terminalResizeObserver = new ResizeObserver(() => {
fitAddon.fit();
})
terminalResizeObserver.observe(terminalContainer.value);
}
});
const chip = ref("");
const programBaud = ref("115200");
const programBaudOption = [
{text: '115200', value: '115200'},
{text: '230400', value: '230400'},
{text: '460800', value: '460800'},
{text: '921600', value: '921600'},
]
const connectedBaud = ref("")
const isConnected = ref(false)
const serialSupported = ref(false);
const imageOption = [
{
value: '无线DAP-LINK_v0.3.1_esp32c3.bin',
link: '/downloads/wireless_proxy_v0.3.1_esp32c3.bin'
},
]
const imageSelect = ref(imageOption[0]);
let transport: Transport | null;
let esploader: ESPLoader;
const espLoaderTerminal: IEspLoaderTerminal = {
clean() {
terminal.clear();
},
writeLine(data) {
terminal.writeln(data);
},
write(data) {
terminal.write(data);
},
};
async function programConnect() {
if (chip.value === "") {
let port = await navigator.serial.requestPort({});
transport = new Transport(port, true);
}
if (!transport) {
return;
}
try {
const flashOptions: LoaderOptions = {
transport,
romBaudrate: 115200,
baudrate: parseInt(programBaud.value),
terminal: espLoaderTerminal,
};
esploader = new ESPLoader(flashOptions);
chip.value = await esploader.main();
connectedBaud.value = programBaud.value;
isConnected.value = true;
// Temporarily broken
// await esploader.flashId();
} catch (error) {
let message = "";
if (error instanceof Error) {
message = error.message;
}
console.error(error);
terminal.writeln(`Error: ${message}`);
}
binaryLoadStatus.status = "";
binaryLoadStatus.progress = 0;
console.log("Settings done for :" + chip.value);
}
function cleanUp() {
transport = null;
chip.value = "";
}
// async function consoleConnectBtn() {
// if (transport) {
// await transport.disconnect();
// await transport.waitForUnlock(1500);
// }
// terminal.reset();
// cleanUp();
// }
//
// async function consoleResetBtn() {
//
// }
interface IBinImage {
data: string;
address: number;
}
function arrayBufferToBinaryString(buffer: ArrayBuffer) {
const byteArray = new Uint8Array(buffer);
let binaryString = '';
byteArray.forEach((byte) => {
binaryString += String.fromCharCode(byte);
});
return binaryString;
}
async function loadBinaryFile_back(imageLink: string) {
try {
const response = await fetch(imageLink);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.blob();
} catch (error) {
console.error('Error fetching the binary file:', error);
}
}
/* cache the last binary, prevent user spam download */
const lastLoadedBinary = {
link: "",
blob: new Blob(),
}
const binaryLoadStatus = reactive({
status: "未连接",
progress: 0,
});
function updateProgress(loaded: number, total: number) {
binaryLoadStatus.progress = Math.round((loaded / total) * 100);
}
async function loadBinaryFile(imageLink: string) {
if (lastLoadedBinary.link === imageLink) {
return lastLoadedBinary.blob;
}
try {
binaryLoadStatus.progress = 0;
binaryLoadStatus.status = "固件下载中";
const response = await fetch(imageLink);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const contentLength = response.headers.get('content-length');
if (!contentLength) {
throw new Error('Content-Length header is missing');
}
const total = parseInt(contentLength, 10);
let loaded = 0;
// Stream response body
const reader = response.body.getReader();
let chunks = []; // to store chunks of data
let receivedLength = 0; // received that many bytes at the moment
while(true) {
const {done, value} = await reader.read();
if (done) {
break;
}
chunks.push(value);
receivedLength += value.length;
// report progress
loaded += value.length;
updateProgress(loaded, total);
}
// Concatenate chunks into single Uint8Array
let chunksAll = new Uint8Array(receivedLength);
let position = 0;
for(let chunk of chunks) {
chunksAll.set(chunk, position);
position += chunk.length;
}
lastLoadedBinary.link = imageLink;
lastLoadedBinary.blob = new Blob([chunksAll]); // Convert to blob
binaryLoadStatus.status = "固件下载完成";
return lastLoadedBinary.blob;
} catch (error) {
binaryLoadStatus.status = "固件下载错误";
console.error('Error fetching the binary file:', error);
}
}
async function programFlash() {
const fileArray: IBinImage[] = [];
const blob = await loadBinaryFile(imageSelect.value.link);
console.log(blob);
if (blob && blob.size > 100000) {
let data = arrayBufferToBinaryString(await blob.arrayBuffer());
fileArray.push({
data: data,
address: 0x0,
});
} else {
alert("???");
return;
}
binaryLoadStatus.status = "固件烧录中";
try {
const flashOptions: FlashOptions = {
fileArray: fileArray,
flashSize: "keep",
eraseAll: false,
compress: true,
flashMode: "DIO",
flashFreq: programBaud.value,
reportProgress: (fileIndex, written, total) => {
updateProgress(written, total);
console.log(fileIndex, written, total);
},
calculateMD5Hash: (image) => {
const hash = CryptoJS.MD5(CryptoJS.enc.Latin1.parse(image));
return hash.toString(CryptoJS.enc.Hex);
},
};
await esploader.writeFlash(flashOptions);
} catch (e) {
let message = "";
if (e instanceof Error) {
message = e.message;
terminal.writeln(`Error: ${message}`);
}
console.error(e);
}
binaryLoadStatus.status = "固件烧录完成";
terminal.writeln("烧录完成,请断开连接,手动重启");
await resetClick();
}
async function programErase() {
try {
await esploader.eraseFlash();
} catch (e: any) {
console.error(e);
terminal.writeln(`Error: ${e?.message}`);
}
}
async function programDisconnect() {
if (transport) {
await transport.disconnect();
await transport.waitForUnlock(1500);
}
isConnected.value = false;
chip.value = "";
connectedBaud.value = "";
binaryLoadStatus.status = "未连接";
terminal.reset();
}
async function resetClick() {
if (transport) {
await transport.setDTR(false);
await new Promise((resolve) => setTimeout(resolve, 100));
await transport.setDTR(true);
}
}
async function handleFileChange(e: Event) {
const target = e.target as HTMLInputElement
const files = target.files
const fileArray: IBinImage[] = [];
const blob = await loadBinaryFile(imageSelect.value.link);
if (blob) {
let data = arrayBufferToBinaryString(await blob.arrayBuffer());
console.log(blob.size, data);
}
if (files && files.length > 0) {
const file = files[0];
let data: string = arrayBufferToBinaryString(await file.arrayBuffer());
fileArray.push({
data: data,
address: 0x0,
})
console.log(file, data);
}
}
const customColors = [
{ color: '#f56c6c', percentage: 20 },
{ color: '#f5816c', percentage: 30 },
{ color: '#e6a23c', percentage: 40 },
{ color: '#e6be3c', percentage: 60 },
{ color: '#d5e63c', percentage: 80 },
{ color: '#ade63c', percentage: 95 },
{ color: '#019d30', percentage: 100 },
]
</script>
<template>
<div>
<h1>在线ESP32烧录<span v-if="serialSupported">免环境配置免装软件</span></h1>
<el-divider></el-divider>
<div v-show="serialSupported">
<el-alert type="info" class="mb-4" show-icon>
若无法连接请先让ESP32进入下载模式再尝试连接按住BOOT按一下RESET松开BOOT
</el-alert>
<el-form label-width="auto">
<el-form-item label="固件">
<client-only>
<el-select
v-model="imageSelect"
placeholder="选择固件"
>
<el-option
v-for="item in imageOption"
:key="item.value"
:label="item.value"
:value="item"
/>
</el-select>
</client-only>
</el-form-item>
<el-form-item label="波特率">
<client-only>
<el-select
v-model="programBaud"
placeholder="选择波特率"
>
<el-option
v-for="item in programBaudOption"
:key="item.value"
:label="item.value"
:value="item.value"
/>
</el-select>
</client-only>
</el-form-item>
<el-form-item label="操作">
<el-button v-if="!isConnected" @click="programConnect" type="primary">连接</el-button>
<el-button v-show="isConnected" @click="programFlash" type="primary">烧录</el-button>
<el-button v-show="isConnected" @click="programErase" type="danger">全片擦除</el-button>
<el-button v-show="isConnected" @click="programDisconnect" type="info">断开连接</el-button>
</el-form-item>
<el-form-item label="已连接" v-show="isConnected">
<div class="flex gap-2">
<el-tag type="primary">芯片型号 {{ chip }}</el-tag>
<el-tag type="success">波特率 {{ connectedBaud }}</el-tag>
<!-- <el-tag type="info">Tag 3</el-tag>-->
<!-- <el-tag type="warning">Tag 4</el-tag>-->
<!-- <el-tag type="danger">Tag 5</el-tag>-->
</div>
</el-form-item>
<el-form-item label="状态" class="border" v-if="binaryLoadStatus.status">
<div class="flex flex-row w-full">
<el-text class="w-32">{{ binaryLoadStatus.status }}</el-text>
<el-progress :percentage="binaryLoadStatus.progress" :color="customColors" class="w-full"/>
</div>
</el-form-item>
</el-form>
<!-- <input type="file" @change="handleFileChange"/>-->
<!-- <div>-->
<!-- <h1>Console</h1>-->
<!-- <p>Connected to device: {{chip}}</p>-->
<!-- <button @click="consoleConnectBtn">stop</button>-->
<!-- <button @click="consoleResetBtn">reset</button>-->
<!-- </div>-->
<div>
<div id="terminal-container" ref="terminalContainer" class="terminal"></div>
</div>
</div>
<div v-show="!serialSupported">
<div class="text-center">
<a href="/"><el-button type="primary">返回至首页</el-button></a>
</div>
<h2>{{notSupportedMsg}}</h2>
</div>
</div>
</template>
<style scoped>
.terminal {
background-color: black;
}
</style>
<style>
.el-popper {
transition: all 0.05s;
}
</style>