@@ -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
8061040function 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+
11781469async 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