Skip to content

Commit 069fe28

Browse files
authored
支持UVE5欢迎界面写频 (#34)
* 支持UVE5欢迎界面写频 * 支持UVE5固件烧录 * UVE5存储区扩大到512KB
1 parent 40299b2 commit 069fe28

File tree

5 files changed

+426
-28
lines changed

5 files changed

+426
-28
lines changed

src/components/navbar/index.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@
232232
"LOSEHU.*P.*K" : "ltsk.json",
233233
"LOSEHU.*P.*" : "lts.json",
234234
"LOSEHU.*D" : "losehud.json",
235+
"UVE.*" : "losehu124h.json",
235236
"LOSEHU13[0-9].*HS" : "losehu124h.json",
236237
"LOSEHU13[0-9].*H" : "losehu124h.json",
237238
"LOSEHU13[0-9].*KS" : "losehu120k.json",

src/utils/serial.js

Lines changed: 296 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,12 @@ function globalRelease(target = 'all'){
755755
if(globalWriteReader)globalWriteReader.releaseLock()
756756
}
757757
if(target != 'write'){
758-
if(globalReadReader)globalReadReader.releaseLock()
758+
// If a previous read is still pending, releaseLock() may throw.
759+
// Best-effort cancel first to ensure the stream unlocks.
760+
if (globalReadReader) {
761+
try { globalReadReader.cancel(); } catch {}
762+
try { globalReadReader.releaseLock(); } catch {}
763+
}
759764
}
760765
} catch {}
761766
}
@@ -775,8 +780,10 @@ async function connect() {
775780
return null;
776781
}
777782

783+
const baudRate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 38400;
784+
778785
try {
779-
await port.open({ baudRate: 38400 });
786+
await port.open({ baudRate });
780787
return port;
781788
} catch (error) {
782789
if(port.connected && port.readable && port.writable && !port.readable.locked && !port.writable.locked){
@@ -801,6 +808,233 @@ async function disconnect(port) {
801808
console.error('Error closing the serial port:', error);
802809
}
803810
}
811+
const CRC32_TABLE = (() => {
812+
const table = new Uint32Array(256);
813+
for (let i = 0; i < 256; i++) {
814+
let c = i;
815+
for (let j = 0; j < 8; j++) {
816+
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
817+
}
818+
table[i] = c >>> 0;
819+
}
820+
return table;
821+
})();
822+
823+
function crc32(data) {
824+
let crc = 0xFFFFFFFF;
825+
for (let i = 0; i < data.length; i++) {
826+
crc = CRC32_TABLE[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
827+
}
828+
return (crc ^ 0xFFFFFFFF) >>> 0;
829+
}
830+
831+
async function rawWrite(port, bytes) {
832+
if (!port || !port.writable) {
833+
throw new Error('Serial port not writable');
834+
}
835+
globalRelease('write');
836+
const writer = port.writable.getWriter();
837+
globalWriteReader = writer;
838+
try {
839+
const CHUNK = 256;
840+
for (let offset = 0; offset < bytes.length; offset += CHUNK) {
841+
await writer.write(bytes.slice(offset, offset + CHUNK));
842+
}
843+
} finally {
844+
try { writer.releaseLock(); } catch {}
845+
}
846+
}
847+
848+
async function rawReadOnce(port, timeoutMs) {
849+
if (!port || !port.readable) {
850+
throw new Error('Serial port not readable');
851+
}
852+
globalRelease('read');
853+
const reader = port.readable.getReader();
854+
globalReadReader = reader;
855+
let timeoutId;
856+
857+
try {
858+
const result = await Promise.race([
859+
reader.read(),
860+
new Promise((resolve) => {
861+
timeoutId = setTimeout(async () => {
862+
// Do not await cancel(): it may hang when the device/USB stack is in a bad state.
863+
try { reader.cancel(); } catch {}
864+
resolve({ value: undefined, done: false, timeout: true });
865+
}, timeoutMs);
866+
})
867+
]);
868+
if (result && result.done) {
869+
throw new Error('Serial stream closed');
870+
}
871+
return result?.value;
872+
} finally {
873+
clearTimeout(timeoutId);
874+
try { reader.releaseLock(); } catch {}
875+
}
876+
}
877+
878+
async function waitForText(port, needle, timeoutMs, onChunk) {
879+
const decoder = new TextDecoder();
880+
let buffer = '';
881+
const deadline = Date.now() + timeoutMs;
882+
const needles = Array.isArray(needle) ? needle : [needle];
883+
884+
while (Date.now() < deadline) {
885+
const remaining = Math.max(1, deadline - Date.now());
886+
const chunk = await rawReadOnce(port, Math.min(500, remaining));
887+
if (!chunk || chunk.length === 0) {
888+
continue;
889+
}
890+
const text = decoder.decode(chunk);
891+
if (onChunk) onChunk(text);
892+
buffer += text;
893+
if (needles.some((n) => buffer.includes(n))) {
894+
return true;
895+
}
896+
if (buffer.length > 4096) {
897+
buffer = buffer.slice(-4096);
898+
}
899+
}
900+
throw new Error(`Timeout waiting for ${needles.join(' | ')}`);
901+
}
902+
903+
async function readAck(port, timeoutMs) {
904+
const deadline = Date.now() + timeoutMs;
905+
while (Date.now() < deadline) {
906+
const remaining = Math.max(1, deadline - Date.now());
907+
const chunk = await rawReadOnce(port, Math.min(500, remaining));
908+
if (!chunk || chunk.length === 0) continue;
909+
910+
for (let i = 0; i < chunk.length; i++) {
911+
const b = chunk[i];
912+
if (b === 0x41) {
913+
return { ok: true };
914+
}
915+
if (b === 0x45) {
916+
let code;
917+
if (i + 1 < chunk.length) {
918+
code = chunk[i + 1];
919+
} else {
920+
const extra = await rawReadOnce(port, 200);
921+
if (extra && extra.length) code = extra[0];
922+
}
923+
return { ok: false, code };
924+
}
925+
}
926+
}
927+
return { ok: false, timeout: true };
928+
}
929+
930+
async function uve5_flashFirmware(port, firmware, options = {}) {
931+
const {
932+
chunkSize = 2048,
933+
readyTimeoutMs = 30000,
934+
startTimeoutMs = 60000,
935+
ackTimeoutMs = 15000,
936+
retries = 30,
937+
onLog,
938+
onProgress
939+
} = options;
940+
941+
const log = (msg) => {
942+
if (onLog) onLog(msg);
943+
};
944+
945+
const reportProgress = (sent) => {
946+
if (onProgress) onProgress(sent, firmware.length);
947+
};
948+
949+
if (!firmware || firmware.length === 0) {
950+
throw new Error('Empty firmware');
951+
}
952+
if (chunkSize <= 0 || chunkSize > 0xFFFF) {
953+
throw new Error('Invalid chunkSize');
954+
}
955+
956+
const MAGIC = 0x32445055;
957+
const header = new Uint8Array(10);
958+
const headerDv = new DataView(header.buffer);
959+
headerDv.setUint32(0, MAGIC >>> 0, true);
960+
headerDv.setUint32(4, firmware.length >>> 0, true);
961+
headerDv.setUint16(8, chunkSize & 0xFFFF, true);
962+
963+
// Handshake can be flaky on some browsers/USB-UARTs; retry the READY→GO→header→START phase.
964+
const handshakeRetries = 3;
965+
let handshakeOk = false;
966+
for (let hs = 1; hs <= handshakeRetries; hs += 1) {
967+
log(`UVE5: 等待设备 READY... (${hs}/${handshakeRetries})`);
968+
await waitForText(port, 'READY', readyTimeoutMs);
969+
970+
log('UVE5: 发送 GO...');
971+
await rawWrite(port, new TextEncoder().encode('GO\n'));
972+
// Give bootloader a moment to switch from READY spam into header parser.
973+
await sleep(50);
974+
975+
log('UVE5: 发送头信息...');
976+
await rawWrite(port, header);
977+
// Some devices need a tiny gap after header before responding START.
978+
await sleep(20);
979+
980+
log('UVE5: 等待设备 START...');
981+
try {
982+
// 新版 bootloader 会发 STRT(避免包含 'A' 导致误判 ACK)。
983+
// 兼容旧版仍可能发 START。
984+
await waitForText(port, ['STRT', 'START'], startTimeoutMs);
985+
handshakeOk = true;
986+
break;
987+
} catch (e) {
988+
if (hs >= handshakeRetries) throw e;
989+
log('UVE5: 等待 START 超时,重试握手...');
990+
await sleep(200);
991+
}
992+
}
993+
if (!handshakeOk) {
994+
throw new Error('UVE5: 握手失败');
995+
}
996+
997+
let seq = 0;
998+
let offset = 0;
999+
reportProgress(0);
1000+
1001+
while (offset < firmware.length) {
1002+
const len = Math.min(chunkSize, firmware.length - offset);
1003+
const data = firmware.slice(offset, offset + len);
1004+
const crc = crc32(data);
1005+
1006+
const frame = new Uint8Array(4 + 2 + len + 4);
1007+
const dv = new DataView(frame.buffer);
1008+
dv.setUint32(0, seq >>> 0, true);
1009+
dv.setUint16(4, len & 0xFFFF, true);
1010+
frame.set(data, 6);
1011+
dv.setUint32(6 + len, crc >>> 0, true);
1012+
1013+
let attempt = 0;
1014+
while (true) {
1015+
await rawWrite(port, frame);
1016+
const perChunkTimeout = seq === 0 ? Math.max(ackTimeoutMs, 15000) : ackTimeoutMs;
1017+
const ack = await readAck(port, perChunkTimeout);
1018+
if (ack.ok) break;
1019+
attempt += 1;
1020+
const code = ack.code !== undefined ? ack.code : '?';
1021+
log(`UVE5: 块${seq} NAK(E${code}),重试 ${attempt}/${retries}`);
1022+
if (attempt >= retries) {
1023+
throw new Error(`UVE5: 块${seq} 连续失败(E${code}),已放弃`);
1024+
}
1025+
// If device is still waiting for the previous frame to finish timing out,
1026+
// a tiny delay helps prevent piling up bytes.
1027+
await sleep(50);
1028+
}
1029+
1030+
offset += len;
1031+
seq += 1;
1032+
reportProgress(offset);
1033+
}
1034+
1035+
log('UVE5: 数据发送完成');
1036+
reportProgress(firmware.length);
1037+
}
8041038

8051039

8061040
function xor(data) {
@@ -1175,6 +1409,63 @@ async function eeprom_read(port, address, size = 0x80, protocol = "official") {
11751409
}
11761410
}
11771411

1412+
// Shared flash read/write (ESP32 shared partition window)
1413+
// Command format is intentionally the same as the losehu extended EEPROM commands,
1414+
// but with command IDs 0x142B / 0x1438.
1415+
// Note: address here is the shared-partition offset (NOT the 0x02000-mapped logical EEPROM address).
1416+
// UVE5 default partition table uses a 512KiB shared partition: 0x00000..0x7FFFF.
1417+
const SHARED_PARTITION_SIZE = 0x80000;
1418+
1419+
function assertSharedRange(address, size) {
1420+
if (!Number.isFinite(address) || address < 0) {
1421+
throw new Error('shared address must be a non-negative number');
1422+
}
1423+
if (!Number.isFinite(size) || size < 0) {
1424+
throw new Error('shared size must be a non-negative number');
1425+
}
1426+
const end = address + size;
1427+
if (end > SHARED_PARTITION_SIZE) {
1428+
throw new Error(`shared access out of range: 0x${address.toString(16)}..0x${(end - 1).toString(16)} (max 0x${(SHARED_PARTITION_SIZE - 1).toString(16)})`);
1429+
}
1430+
}
1431+
async function shared_read(port, address, size = 0x80) {
1432+
sessionStorage.removeItem('webusb')
1433+
1434+
assertSharedRange(address, size);
1435+
1436+
const address_msb = (address & 0xff00) >> 8;
1437+
const address_lsb = address & 0xff;
1438+
1439+
const address_msb_h = (address & 0xff000000) >> 24;
1440+
const address_lsb_h = (address & 0xff0000) >> 16;
1441+
1442+
const packet = new Uint8Array([0x2b, 0x14, 0x08, 0x00, address_lsb_h, address_msb_h, size, 0x00, 0xff, 0xff, 0xff, 0xff, address_lsb, address_msb]);
1443+
await sendPacket(port, packet);
1444+
1445+
const response = await readPacket(port, 0x1c);
1446+
const data = new Uint8Array(response.slice(8));
1447+
return data;
1448+
}
1449+
1450+
async function shared_write(port, address, input, size = 0x80) {
1451+
assertSharedRange(address, size);
1452+
1453+
const address_msb = (address & 0xff00) >> 8;
1454+
const address_lsb = address & 0xff;
1455+
1456+
const address_msb_h = (address & 0xff000000) >> 24;
1457+
const address_lsb_h = (address & 0xff0000) >> 16;
1458+
1459+
const packet = new Uint8Array([0x38, 0x14, 0x1c, 0x00, address_lsb_h, address_msb_h, size + 2, 0x00, 0xff, 0xff, 0xff, 0xff, address_lsb, address_msb]);
1460+
const mergedArray = new Uint8Array(packet.length + input.length);
1461+
mergedArray.set(packet);
1462+
mergedArray.set(input, packet.length);
1463+
1464+
await sendPacket(port, mergedArray);
1465+
await readPacket(port, 0x1e);
1466+
return true;
1467+
}
1468+
11781469
async function eeprom_write(port, address, input, size = 0x80, protocol = "official") {
11791470
if (protocol == "official") {
11801471
// packet format: uint16 ID, uint16 length, uint16 address, uint8 size, uint8 padding, uint32 timestamp
@@ -1591,12 +1882,15 @@ export {
15911882
eeprom_reboot,
15921883
check_eeprom,
15931884
eeprom_write,
1885+
shared_read,
1886+
shared_write,
15941887
flash_flashFirmware,
15951888
flash_generateCommand,
15961889
flash_generateK1Command,
15971890
unpackVersion,
15981891
unpack,
15991892
readPacketNoVerify,
1893+
uve5_flashFirmware,
16001894
sendSMSPacket,
16011895
readSMSPacket
16021896
}

0 commit comments

Comments
 (0)