script.js 215 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498
  1. /**
  2. * MicroLink Web Serial Terminal
  3. * 使用 Web Serial API 实现浏览器直接访问串口
  4. */
  5. class MicroLinkTerminal {
  6. constructor() {
  7. this.port = null;
  8. this.reader = null;
  9. this.writer = null;
  10. this.isConnected = false;
  11. this.customCommands = [];
  12. // 数据缓冲相关
  13. this.dataBuffer = '';
  14. this.lineTimeoutId = null;
  15. this.bufferStartTime = null; // 记录缓冲区开始接收数据的时间
  16. // 终端输入相关
  17. this.commandHistory = [];
  18. this.historyIndex = -1;
  19. this.logMessages = [];
  20. // 虚拟终端配置
  21. this.virtualTerminalMode = true; // 启用虚拟终端模式,实时发送按键
  22. this.virtualTerminalModeOriginalValue = true; // 保存用户原始设置值
  23. this.currentLine = ''; // 当前行缓冲
  24. this.initializeElements();
  25. this.bindEvents();
  26. this.updateSendOptions(); // 初始化发送选项状态
  27. this.loadParameters();
  28. this.checkWebSerialSupport();
  29. }
  30. initializeElements() {
  31. // 串口配置元素
  32. this.baudRateSelect = document.getElementById('baudRate');
  33. this.customBaudRateInput = document.getElementById('customBaudRate');
  34. this.dataBitsSelect = document.getElementById('dataBits');
  35. this.paritySelect = document.getElementById('parity');
  36. this.stopBitsSelect = document.getElementById('stopBits');
  37. // 连接控制元素
  38. this.connectBtn = document.getElementById('connectBtn');
  39. this.disconnectBtn = document.getElementById('disconnectBtn');
  40. this.clearBtn = document.getElementById('clearBtn');
  41. this.saveLogBtn = document.getElementById('saveLogBtn');
  42. this.connectionStatus = document.getElementById('connectionStatus');
  43. // 显示选项元素
  44. this.hexModeCheckbox = document.getElementById('hexMode');
  45. this.showTimestampCheckbox = document.getElementById('showTimestamp');
  46. this.autoScrollCheckbox = document.getElementById('autoScroll');
  47. this.enableBufferCheckbox = document.getElementById('enableBuffer');
  48. this.virtualTerminalModeCheckbox = document.getElementById('virtualTerminalMode');
  49. this.processAnsiSequencesCheckbox = document.getElementById('processAnsiSequences');
  50. this.debugModeCheckbox = document.getElementById('debugMode');
  51. this.lineTimeoutInput = document.getElementById('lineTimeout');
  52. // 终端元素
  53. this.terminal = document.getElementById('terminal');
  54. this.terminalOutput = document.getElementById('terminalOutput');
  55. this.terminalInputField = document.getElementById('terminalInputField');
  56. // API 控制元素
  57. this.rttAddrInput = document.getElementById('rttAddr');
  58. this.rttSizeInput = document.getElementById('rttSize');
  59. this.rttChannelInput = document.getElementById('rttChannel');
  60. this.startRTTBtn = document.getElementById('startRTT');
  61. this.stopRTTBtn = document.getElementById('stopRTT');
  62. this.svAddrInput = document.getElementById('svAddr');
  63. this.svSizeInput = document.getElementById('svSize');
  64. this.svChannelInput = document.getElementById('svChannel');
  65. this.startSystemViewBtn = document.getElementById('startSystemView');
  66. this.flmPathInput = document.getElementById('flmPath');
  67. this.baseAddrInput = document.getElementById('baseAddr');
  68. this.ramAddrInput = document.getElementById('ramAddr');
  69. this.loadFLMBtn = document.getElementById('loadFLM');
  70. this.binPathInput = document.getElementById('binPath');
  71. this.binAddrInput = document.getElementById('binAddr');
  72. this.loadBinBtn = document.getElementById('loadBin');
  73. this.offlineDownloadBtn = document.getElementById('offlineDownload');
  74. this.ymodemFileInput = document.getElementById('ymodemFile');
  75. this.ymodemSendBtn = document.getElementById('ymodemSend');
  76. this.customCommandInput = document.getElementById('customCommand');
  77. this.sendCustomBtn = document.getElementById('sendCustom');
  78. this.addCustomBtn = document.getElementById('addCustom');
  79. this.customCommandsList = document.getElementById('customCommandsList');
  80. this.saveParamsBtn = document.getElementById('saveParams');
  81. this.saveToFileBtn = document.getElementById('saveToFile');
  82. this.loadParamsBtn = document.getElementById('loadParams');
  83. this.resetParamsBtn = document.getElementById('resetParams');
  84. this.loadConfigFileBtn = document.getElementById('loadConfigFile');
  85. this.configFileInput = document.getElementById('configFileInput');
  86. }
  87. bindEvents() {
  88. // 波特率选择事件
  89. this.baudRateSelect.addEventListener('change', () => {
  90. if (this.baudRateSelect.value === 'custom') {
  91. this.customBaudRateInput.style.display = 'block';
  92. } else {
  93. this.customBaudRateInput.style.display = 'none';
  94. }
  95. });
  96. // 连接控制事件
  97. this.connectBtn.addEventListener('click', () => this.connectSerial());
  98. this.disconnectBtn.addEventListener('click', () => this.disconnectSerial());
  99. this.clearBtn.addEventListener('click', () => this.clearTerminal());
  100. this.saveLogBtn.addEventListener('click', () => this.saveLog());
  101. // 终端输入事件
  102. this.terminalInputField.addEventListener('keydown', (e) => this.handleTerminalInput(e));
  103. this.terminalInputField.addEventListener('focus', () => this.scrollToBottom());
  104. // 终端发送按钮事件
  105. this.terminalSendBtn = document.getElementById('terminalSendBtn');
  106. if (this.terminalSendBtn) {
  107. this.terminalSendBtn.addEventListener('click', () => this.executeTerminalCommand());
  108. }
  109. // 虚拟终端配置事件
  110. this.virtualTerminalModeCheckbox.addEventListener('change', () => {
  111. this.virtualTerminalMode = this.virtualTerminalModeCheckbox.checked;
  112. // 只有在非HEX模式下才保存用户的原始设置
  113. if (!this.hexModeCheckbox.checked) {
  114. this.virtualTerminalModeOriginalValue = this.virtualTerminalModeCheckbox.checked;
  115. }
  116. this.updateTerminalPlaceholder();
  117. });
  118. // HEX模式切换事件
  119. this.hexModeCheckbox.addEventListener('change', () => {
  120. this.updateSendOptions();
  121. });
  122. // API 控制事件
  123. this.startRTTBtn.addEventListener('click', () => this.startRTT());
  124. this.stopRTTBtn.addEventListener('click', () => this.stopRTT());
  125. this.startSystemViewBtn.addEventListener('click', () => this.startSystemView());
  126. this.loadFLMBtn.addEventListener('click', () => this.loadFLM());
  127. this.loadBinBtn.addEventListener('click', () => this.loadBin());
  128. this.offlineDownloadBtn.addEventListener('click', () => this.offlineDownload());
  129. this.ymodemSendBtn.addEventListener('click', () => this.ymodemSend());
  130. this.sendCustomBtn.addEventListener('click', () => this.sendCustomCommand());
  131. this.addCustomBtn.addEventListener('click', () => this.addCustomCommand());
  132. // 参数管理事件
  133. this.saveParamsBtn.addEventListener('click', () => this.saveParameters());
  134. this.saveToFileBtn.addEventListener('click', () => this.saveParametersToFile());
  135. this.loadParamsBtn.addEventListener('click', () => this.loadParameters());
  136. this.resetParamsBtn.addEventListener('click', () => this.resetParameters());
  137. this.loadConfigFileBtn.addEventListener('click', () => this.loadConfigFile());
  138. this.configFileInput.addEventListener('change', (e) => this.handleConfigFileSelect(e));
  139. // 统一YMODEM相关按钮事件绑定
  140. const flmYmodemSendBtn = document.getElementById('flmYmodemSendBtn');
  141. if (flmYmodemSendBtn) {
  142. flmYmodemSendBtn.addEventListener('click', () => this.handleFlmYmodemSend());
  143. }
  144. // Python发送按钮事件绑定在setupPythonScriptPanel中处理
  145. }
  146. checkWebSerialSupport() {
  147. if (!('serial' in navigator)) {
  148. this.addMessage('错误: 您的浏览器不支持 Web Serial API。请使用 Chrome 89+ 或 Edge 89+', 'error');
  149. this.connectBtn.disabled = true;
  150. this.connectBtn.innerHTML = '<i class="fas fa-times"></i> 不支持';
  151. }
  152. }
  153. async connectSerial() {
  154. try {
  155. // 请求串口权限
  156. this.port = await navigator.serial.requestPort();
  157. // 获取串口配置
  158. const baudRate = this.baudRateSelect.value === 'custom'
  159. ? parseInt(this.customBaudRateInput.value)
  160. : parseInt(this.baudRateSelect.value);
  161. const dataBits = parseInt(this.dataBitsSelect.value);
  162. const stopBits = parseInt(this.stopBitsSelect.value);
  163. const parity = this.paritySelect.value;
  164. // 打开串口
  165. await this.port.open({
  166. baudRate: baudRate,
  167. dataBits: dataBits,
  168. stopBits: stopBits,
  169. parity: parity
  170. });
  171. this.isConnected = true;
  172. this.updateConnectionStatus(true);
  173. // 格式化校验位显示
  174. const parityDisplay = parity === 'none' ? 'N' : parity === 'even' ? 'E' : 'O';
  175. this.addMessage(`串口连接成功 - 波特率: ${baudRate}, 数据位: ${dataBits}${parityDisplay}${stopBits}`, 'info');
  176. // 开始读取数据
  177. this.startReading();
  178. } catch (error) {
  179. this.addMessage(`连接失败: ${error.message}`, 'error');
  180. }
  181. }
  182. async disconnectSerial() {
  183. try {
  184. // 先设置断开标志,停止读取循环
  185. this.isConnected = false;
  186. // 释放reader锁
  187. if (this.reader) {
  188. try {
  189. await this.reader.cancel();
  190. } catch (e) {
  191. // 忽略cancel错误,继续释放锁
  192. console.log('Reader cancel error:', e);
  193. }
  194. try {
  195. this.reader.releaseLock();
  196. } catch (e) {
  197. // 忽略releaseLock错误
  198. console.log('Reader releaseLock error:', e);
  199. }
  200. this.reader = null;
  201. }
  202. // 释放writer锁
  203. if (this.writer) {
  204. try {
  205. this.writer.releaseLock();
  206. } catch (e) {
  207. console.log('Writer releaseLock error:', e);
  208. }
  209. this.writer = null;
  210. }
  211. // 关闭串口
  212. if (this.port) {
  213. await this.port.close();
  214. this.port = null;
  215. }
  216. this.updateConnectionStatus(false);
  217. // 清理数据缓冲区
  218. this.flushBuffer();
  219. this.addMessage('串口已断开', 'info');
  220. } catch (error) {
  221. this.addMessage(`断开连接失败: ${error.message}`, 'error');
  222. // 强制重置状态
  223. this.isConnected = false;
  224. this.reader = null;
  225. this.writer = null;
  226. this.port = null;
  227. this.updateConnectionStatus(false);
  228. }
  229. }
  230. async startReading() {
  231. if (!this.port) return;
  232. try {
  233. this.reader = this.port.readable.getReader();
  234. while (this.isConnected) {
  235. const { value, done } = await this.reader.read();
  236. if (done) break;
  237. this.handleReceivedData(value);
  238. }
  239. } catch (error) {
  240. if (this.isConnected) {
  241. this.addMessage(`读取数据错误: ${error.message}`, 'error');
  242. }
  243. } finally {
  244. if (this.reader) {
  245. try {
  246. this.reader.releaseLock();
  247. } catch (e) {
  248. console.log('Reader releaseLock in finally:', e);
  249. }
  250. this.reader = null;
  251. }
  252. }
  253. }
  254. handleReceivedData(data) {
  255. // 记录数据接收时间
  256. const receiveTime = new Date();
  257. // 检查是否处于曲线绘制模式
  258. if (chartDrawEnabled && window.chartData) {
  259. // 曲线绘制模式:直接处理串口数据,不显示在主监控
  260. // console.log(`[曲线模式] 接收到 ${data.length} 字节数据:`, Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '));
  261. // 处理分片数据,确保4字节对齐
  262. handleRealtimeHexDataChunkedSimple(data);
  263. // 不显示在主监控,直接返回
  264. return;
  265. }
  266. if (this.hexModeCheckbox.checked) {
  267. // HEX 模式显示
  268. const hexText = Array.from(data)
  269. .map(byte => byte.toString(16).padStart(2, '0').toUpperCase())
  270. .join(' ');
  271. if (this.enableBufferCheckbox.checked) {
  272. // HEX模式也使用缓冲和超时机制
  273. this.bufferHexData(hexText);
  274. } else {
  275. // 直接显示,不缓冲
  276. this.addMessage(hexText, 'received', receiveTime);
  277. }
  278. } else {
  279. // 文本模式显示
  280. const text = new TextDecoder().decode(data);
  281. if (this.enableBufferCheckbox.checked) {
  282. // 使用缓冲和超时机制
  283. this.bufferData(text);
  284. } else {
  285. // 直接显示,不缓冲,使用接收时间作为时间戳
  286. this.addMessage(text, 'received', receiveTime);
  287. }
  288. }
  289. }
  290. bufferData(text) {
  291. // 调试模式:显示接收到的原始数据
  292. if (this.debugModeCheckbox.checked) {
  293. const debugInfo = `[DEBUG] 接收数据 (${text.length}字节): ${JSON.stringify(text)}`;
  294. this.addMessage(debugInfo, 'info');
  295. }
  296. // 记录缓冲区开始时间(只在缓冲区为空时记录)
  297. if (this.dataBuffer.length === 0) {
  298. this.bufferStartTime = new Date();
  299. }
  300. // 将新数据添加到缓冲区
  301. this.dataBuffer += text;
  302. // 清除之前的超时(这是关键!每次新数据都重置超时)
  303. if (this.lineTimeoutId) {
  304. clearTimeout(this.lineTimeoutId);
  305. this.lineTimeoutId = null;
  306. if (this.debugModeCheckbox.checked) {
  307. this.addMessage('[DEBUG] 重置超时计时器', 'info');
  308. }
  309. }
  310. // 检查是否包含明确的结束标志
  311. const hasLineEnding = this.dataBuffer.includes('\n') || this.dataBuffer.includes('\r');
  312. // 检查是否是单独的shell提示符(应该保持在同一行)
  313. const isStandalonePrompt = this.dataBuffer.match(/^sh:\/\$\s*$/) ||
  314. this.dataBuffer.match(/^>>>\s*$/) ||
  315. this.dataBuffer.match(/^>\s*$/);
  316. // 检查是否包含完整的命令输出(以换行符+提示符结尾)
  317. const hasCompleteOutput = this.dataBuffer.match(/\n\s*sh:\/\$\s*$/) ||
  318. this.dataBuffer.match(/\r\n\s*sh:\/\$\s*$/) ||
  319. this.dataBuffer.match(/\n\s*>>>\s*$/) ||
  320. this.dataBuffer.match(/\r\n\s*>>>\s*$/);
  321. // 检查是否包含完整的命令列表结束
  322. const hasCommandListEnd = this.dataBuffer.includes('Command List:') &&
  323. (this.dataBuffer.includes('\nsh:/$ ') || this.dataBuffer.includes('\r\nsh:/$ '));
  324. // 检查缓冲区大小,如果太大就强制刷新
  325. const bufferTooLarge = this.dataBuffer.length > 2000;
  326. // 立即刷新的条件
  327. if (hasCompleteOutput || hasCommandListEnd || bufferTooLarge) {
  328. // 立即刷新,不等待超时
  329. this.flushBuffer();
  330. return;
  331. }
  332. // 如果是单独的提示符,不立即刷新,等待后续内容
  333. if (isStandalonePrompt) {
  334. // 使用较长的超时等待后续内容
  335. const timeout = parseInt(this.lineTimeoutInput.value) || 50;
  336. this.lineTimeoutId = setTimeout(() => {
  337. this.flushBuffer();
  338. }, timeout * 2); // 使用双倍超时时间
  339. return;
  340. }
  341. // 如果包含换行符,检查是否是完整的行
  342. if (hasLineEnding) {
  343. // 检查是否以换行符结尾(完整的行)
  344. const endsWithNewline = this.dataBuffer.endsWith('\n') || this.dataBuffer.endsWith('\r\n') || this.dataBuffer.endsWith('\r');
  345. if (endsWithNewline) {
  346. // 完整的行,使用较短的超时
  347. if (this.debugModeCheckbox.checked) {
  348. this.addMessage(`[DEBUG] 检测到完整行,使用短超时(10ms)`, 'info');
  349. }
  350. this.lineTimeoutId = setTimeout(() => {
  351. this.flushBuffer();
  352. }, 10);
  353. } else {
  354. // 包含换行符但不以换行符结尾,可能还有更多数据,使用配置的超时
  355. const timeout = parseInt(this.lineTimeoutInput.value) || 50;
  356. if (this.debugModeCheckbox.checked) {
  357. this.addMessage(`[DEBUG] 包含换行符但不完整,使用配置超时(${timeout}ms)`, 'info');
  358. }
  359. this.lineTimeoutId = setTimeout(() => {
  360. this.flushBuffer();
  361. }, timeout);
  362. }
  363. return;
  364. }
  365. // 默认情况下,设置超时等待更多数据
  366. const baseTimeout = parseInt(this.lineTimeoutInput.value) || 50;
  367. // 根据数据特征调整超时时间
  368. let timeout = baseTimeout;
  369. // 如果包含ANSI序列,使用更短的超时
  370. if (this.dataBuffer.includes('\x1b[')) {
  371. timeout = Math.min(baseTimeout, 20);
  372. if (this.debugModeCheckbox.checked) {
  373. this.addMessage(`[DEBUG] 检测到ANSI序列,调整超时为${timeout}ms`, 'info');
  374. }
  375. }
  376. // 如果数据看起来像是命令输出的一部分,使用更短的超时
  377. if (this.dataBuffer.includes('CMD') || this.dataBuffer.includes('--------')) {
  378. timeout = Math.min(baseTimeout, 15);
  379. if (this.debugModeCheckbox.checked) {
  380. this.addMessage(`[DEBUG] 检测到命令输出,调整超时为${timeout}ms`, 'info');
  381. }
  382. }
  383. if (this.debugModeCheckbox.checked) {
  384. this.addMessage(`[DEBUG] 设置默认超时: ${timeout}ms (基础: ${baseTimeout}ms)`, 'info');
  385. }
  386. this.lineTimeoutId = setTimeout(() => {
  387. this.flushBuffer();
  388. }, timeout);
  389. }
  390. bufferHexData(hexText) {
  391. // 调试模式:显示接收到的原始HEX数据
  392. if (this.debugModeCheckbox.checked) {
  393. const debugInfo = `[DEBUG] 接收HEX数据: ${hexText}`;
  394. this.addMessage(debugInfo, 'info');
  395. }
  396. // 记录缓冲区开始时间(只在缓冲区为空时记录)
  397. if (this.dataBuffer.length === 0) {
  398. this.bufferStartTime = new Date();
  399. }
  400. // 将新的HEX数据添加到缓冲区(用空格分隔)
  401. if (this.dataBuffer.length > 0) {
  402. this.dataBuffer += ' ' + hexText;
  403. } else {
  404. this.dataBuffer = hexText;
  405. }
  406. // 清除之前的超时(每次新数据都重置超时)
  407. if (this.lineTimeoutId) {
  408. clearTimeout(this.lineTimeoutId);
  409. this.lineTimeoutId = null;
  410. if (this.debugModeCheckbox.checked) {
  411. this.addMessage('[DEBUG] 重置HEX超时计时器', 'info');
  412. }
  413. }
  414. // HEX模式下的缓冲逻辑相对简单,主要基于超时
  415. const timeout = parseInt(this.lineTimeoutInput.value) || 50;
  416. // 检查缓冲区大小,如果太大就强制刷新
  417. const bufferTooLarge = this.dataBuffer.length > 1000; // HEX数据较长,适当增加限制
  418. if (bufferTooLarge) {
  419. if (this.debugModeCheckbox.checked) {
  420. this.addMessage('[DEBUG] HEX缓冲区过大,强制刷新', 'info');
  421. }
  422. this.flushBuffer();
  423. return;
  424. }
  425. if (this.debugModeCheckbox.checked) {
  426. this.addMessage(`[DEBUG] 设置HEX超时: ${timeout}ms`, 'info');
  427. }
  428. this.lineTimeoutId = setTimeout(() => {
  429. this.flushBuffer();
  430. }, timeout);
  431. }
  432. flushBuffer() {
  433. if (this.dataBuffer.length > 0) {
  434. if (this.debugModeCheckbox.checked) {
  435. this.addMessage(`[DEBUG] 刷新缓冲区 (${this.dataBuffer.length}字节)`, 'info');
  436. }
  437. // 根据配置决定是否处理ANSI序列
  438. let processedData = this.processAnsiSequencesCheckbox.checked ?
  439. this.processAnsiSequences(this.dataBuffer) :
  440. this.dataBuffer;
  441. // 显示处理后的数据,使用缓冲区开始时间作为时间戳
  442. this.addMessage(processedData, 'received', this.bufferStartTime);
  443. // 清空缓冲区和时间戳
  444. this.dataBuffer = '';
  445. this.bufferStartTime = null;
  446. }
  447. // 清除超时
  448. if (this.lineTimeoutId) {
  449. clearTimeout(this.lineTimeoutId);
  450. this.lineTimeoutId = null;
  451. }
  452. }
  453. processAnsiSequences(text) {
  454. // 处理ANSI转义序列,使其在HTML中正确显示
  455. let processed = text;
  456. // 首先处理特殊的控制序列
  457. // 处理 [2K (清除整行)
  458. processed = processed.replace(/\x1b\[2K/g, '');
  459. // 处理光标移动序列
  460. processed = processed.replace(/\x1b\[[0-9]+;[0-9]+H/g, '');
  461. processed = processed.replace(/\x1b\[[0-9]+;[0-9]+f/g, '');
  462. // 处理回车符覆盖行为(这是关键!)
  463. // 当遇到 \r 但没有 \n 时,表示要覆盖当前行
  464. processed = this.processCarriageReturn(processed);
  465. // 处理ANSI颜色和格式代码
  466. processed = this.processAnsiColors(processed);
  467. // 移除其他未处理的ANSI转义序列
  468. processed = processed.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
  469. // 移除一些控制字符,但保留换行符和制表符
  470. processed = processed.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
  471. // 清理多余的空行(连续的换行符)
  472. processed = processed.replace(/\n{3,}/g, '\n\n');
  473. return processed;
  474. }
  475. processCarriageReturn(text) {
  476. // 处理回车符的覆盖行为
  477. let processed = text;
  478. // 检查是否包含需要处理的覆盖模式
  479. if (processed.includes('\r') && !processed.includes('\r\n')) {
  480. // 只处理特定的shell提示符覆盖模式
  481. // 模式1: 调试信息被提示符覆盖 (如: "2025-05-30 11:21:01 DEBUG: MQTT...\rsh:/$ ")
  482. processed = processed.replace(/(.*DEBUG[^\r]*)\r(sh:\/\$\s*)/g, (match, content, prompt) => {
  483. // 如果调试信息很长,保留它并添加换行,然后显示提示符
  484. if (content.length > 10) {
  485. return content + '\n' + prompt;
  486. } else {
  487. return prompt; // 短内容直接被覆盖
  488. }
  489. });
  490. // 模式2: 其他内容被提示符覆盖,但保留有意义的内容
  491. processed = processed.replace(/([^\r\n]{10,})\r(sh:\/\$\s*|>>>\s*|>\s*)$/gm, '$1\n$2');
  492. // 模式3: 短内容被提示符覆盖(真正的覆盖行为)
  493. processed = processed.replace(/([^\r\n]{1,9})\r(sh:\/\$\s*|>>>\s*|>\s*)$/gm, '$2');
  494. }
  495. return processed;
  496. }
  497. processAnsiColors(text) {
  498. let processed = text;
  499. // 定义ANSI颜色映射(使用终端友好的颜色)
  500. const ansiColors = {
  501. // 重置
  502. '0': { action: 'reset' },
  503. // 文本样式
  504. '1': { style: 'font-weight: bold;' }, // 粗体
  505. '2': { style: 'opacity: 0.6;' }, // 暗淡
  506. '3': { style: 'font-style: italic;' }, // 斜体
  507. '4': { style: 'text-decoration: underline;' }, // 下划线
  508. '5': { style: 'animation: blink 1s infinite;' }, // 闪烁
  509. '7': { style: 'filter: invert(1);' }, // 反色
  510. '9': { style: 'text-decoration: line-through;' }, // 删除线
  511. // 前景色(标准颜色)
  512. '30': { color: '#2e3436' }, // 黑色
  513. '31': { color: '#cc0000' }, // 红色
  514. '32': { color: '#4e9a06' }, // 绿色
  515. '33': { color: '#c4a000' }, // 黄色
  516. '34': { color: '#3465a4' }, // 蓝色
  517. '35': { color: '#75507b' }, // 洋红
  518. '36': { color: '#06989a' }, // 青色
  519. '37': { color: '#d3d7cf' }, // 白色
  520. // 前景色(高亮颜色)
  521. '90': { color: '#555753' }, // 亮黑色(灰色)
  522. '91': { color: '#ef2929' }, // 亮红色
  523. '92': { color: '#8ae234' }, // 亮绿色
  524. '93': { color: '#fce94f' }, // 亮黄色
  525. '94': { color: '#729fcf' }, // 亮蓝色
  526. '95': { color: '#ad7fa8' }, // 亮洋红
  527. '96': { color: '#34e2e2' }, // 亮青色
  528. '97': { color: '#eeeeec' }, // 亮白色
  529. // 背景色(标准颜色)
  530. '40': { backgroundColor: '#2e3436' }, // 黑色背景
  531. '41': { backgroundColor: '#cc0000' }, // 红色背景
  532. '42': { backgroundColor: '#4e9a06' }, // 绿色背景
  533. '43': { backgroundColor: '#c4a000' }, // 黄色背景
  534. '44': { backgroundColor: '#3465a4' }, // 蓝色背景
  535. '45': { backgroundColor: '#75507b' }, // 洋红背景
  536. '46': { backgroundColor: '#06989a' }, // 青色背景
  537. '47': { backgroundColor: '#d3d7cf' }, // 白色背景
  538. // 背景色(高亮颜色)
  539. '100': { backgroundColor: '#555753' }, // 亮黑色背景
  540. '101': { backgroundColor: '#ef2929' }, // 亮红色背景
  541. '102': { backgroundColor: '#8ae234' }, // 亮绿色背景
  542. '103': { backgroundColor: '#fce94f' }, // 亮黄色背景
  543. '104': { backgroundColor: '#729fcf' }, // 亮蓝色背景
  544. '105': { backgroundColor: '#ad7fa8' }, // 亮洋红背景
  545. '106': { backgroundColor: '#34e2e2' }, // 亮青色背景
  546. '107': { backgroundColor: '#eeeeec' }, // 亮白色背景
  547. };
  548. // 处理ANSI转义序列
  549. processed = processed.replace(/\x1b\[([0-9;]*)m/g, (match, codes) => {
  550. if (!codes) codes = '0'; // 空代码默认为重置
  551. const codeList = codes.split(';');
  552. let styles = [];
  553. let hasReset = false;
  554. let i = 0;
  555. while (i < codeList.length) {
  556. const code = codeList[i];
  557. // 处理256色和RGB颜色
  558. if (code === '38' || code === '48') { // 前景色或背景色
  559. const isBackground = code === '48';
  560. i++;
  561. if (i < codeList.length && codeList[i] === '5') {
  562. // 256色模式: ESC[38;5;n m 或 ESC[48;5;n m
  563. i++;
  564. if (i < codeList.length) {
  565. const colorIndex = parseInt(codeList[i]);
  566. const color = this.get256Color(colorIndex);
  567. if (isBackground) {
  568. styles.push(`background-color: ${color}`);
  569. } else {
  570. styles.push(`color: ${color}`);
  571. }
  572. }
  573. } else if (i < codeList.length && codeList[i] === '2') {
  574. // RGB模式: ESC[38;2;r;g;b m 或 ESC[48;2;r;g;b m
  575. i++;
  576. if (i + 2 < codeList.length) {
  577. const r = parseInt(codeList[i]);
  578. const g = parseInt(codeList[i + 1]);
  579. const b = parseInt(codeList[i + 2]);
  580. const color = `rgb(${r}, ${g}, ${b})`;
  581. if (isBackground) {
  582. styles.push(`background-color: ${color}`);
  583. } else {
  584. styles.push(`color: ${color}`);
  585. }
  586. i += 2;
  587. }
  588. }
  589. } else {
  590. // 处理标准ANSI代码
  591. const ansiCode = ansiColors[code];
  592. if (ansiCode) {
  593. if (ansiCode.action === 'reset') {
  594. hasReset = true;
  595. break;
  596. } else {
  597. if (ansiCode.color) styles.push(`color: ${ansiCode.color}`);
  598. if (ansiCode.backgroundColor) styles.push(`background-color: ${ansiCode.backgroundColor}`);
  599. if (ansiCode.style) styles.push(ansiCode.style);
  600. }
  601. }
  602. }
  603. i++;
  604. }
  605. if (hasReset) {
  606. return '</span>';
  607. } else if (styles.length > 0) {
  608. return `<span style="${styles.join('; ')};">`;
  609. } else {
  610. return ''; // 未知代码,移除
  611. }
  612. });
  613. return processed;
  614. }
  615. get256Color(index) {
  616. // 256色调色板
  617. if (index < 16) {
  618. // 标准16色
  619. const colors = [
  620. '#000000', '#800000', '#008000', '#808000', '#000080', '#800080', '#008080', '#c0c0c0',
  621. '#808080', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff'
  622. ];
  623. return colors[index];
  624. } else if (index < 232) {
  625. // 216色立方体 (6x6x6)
  626. const i = index - 16;
  627. const r = Math.floor(i / 36);
  628. const g = Math.floor((i % 36) / 6);
  629. const b = i % 6;
  630. const toHex = (n) => {
  631. const values = [0, 95, 135, 175, 215, 255];
  632. return values[n].toString(16).padStart(2, '0');
  633. };
  634. return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
  635. } else {
  636. // 24级灰度
  637. const gray = 8 + (index - 232) * 10;
  638. const hex = gray.toString(16).padStart(2, '0');
  639. return `#${hex}${hex}${hex}`;
  640. }
  641. }
  642. escapeRegExp(string) {
  643. return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  644. }
  645. // MicroLink API 方法
  646. async startRTT() {
  647. const addr = this.rttAddrInput.value;
  648. const size = this.rttSizeInput.value;
  649. const channel = this.rttChannelInput.value;
  650. const command = `RTTView.start(${addr}, ${size}, ${channel})`;
  651. await this.sendCommand(command);
  652. }
  653. async stopRTT() {
  654. const command = 'RTTView.stop()';
  655. await this.sendCommand(command);
  656. }
  657. async startSystemView() {
  658. const addr = this.svAddrInput.value;
  659. const size = this.svSizeInput.value;
  660. const channel = this.svChannelInput.value;
  661. const command = `SystemView.start(${addr}, ${size}, ${channel})`;
  662. await this.sendCommand(command);
  663. }
  664. async loadFLM() {
  665. const flmPath = this.flmPathInput.value;
  666. const baseAddr = this.baseAddrInput.value;
  667. const ramAddr = this.ramAddrInput.value;
  668. const command = `ReadFlm.load("${flmPath}", ${baseAddr}, ${ramAddr})`;
  669. await this.sendCommand(command);
  670. }
  671. async loadBin() {
  672. const binPath = this.binPathInput.value;
  673. const binAddr = this.binAddrInput.value;
  674. const command = `load.bin("${binPath}", ${binAddr})`;
  675. await this.sendCommand(command);
  676. }
  677. async offlineDownload() {
  678. const command = 'load.offline()';
  679. await this.sendCommand(command);
  680. }
  681. async ymodemSend() {
  682. const filePath = this.ymodemFileInput.value;
  683. const command = `ym.send("${filePath}")`;
  684. await this.sendCommand(command);
  685. }
  686. async sendCustomCommand() {
  687. const command = this.customCommandInput.value.trim();
  688. if (command) {
  689. if (this.hexModeCheckbox.checked) {
  690. // HEX模式:发送十六进制数据
  691. await this.sendHexData(command);
  692. } else {
  693. // 文本模式:发送普通命令
  694. await this.sendCommand(command);
  695. }
  696. }
  697. }
  698. async sendCommand(command) {
  699. if (!this.isConnected) {
  700. this.addMessage('请先连接串口', 'error');
  701. return;
  702. }
  703. try {
  704. const dataToSend = new TextEncoder().encode(command + '\r\n');
  705. if (!this.writer) {
  706. this.writer = this.port.writable.getWriter();
  707. }
  708. await this.writer.write(dataToSend);
  709. this.addMessage(`命令: ${command}`, 'sent');
  710. } catch (error) {
  711. this.addMessage(`发送命令失败: ${error.message}`, 'error');
  712. }
  713. }
  714. addCustomCommand() {
  715. const command = this.customCommandInput.value.trim();
  716. if (command && !this.customCommands.includes(command)) {
  717. this.customCommands.push(command);
  718. this.updateCustomCommandsList();
  719. this.customCommandInput.value = '';
  720. }
  721. }
  722. updateCustomCommandsList() {
  723. this.customCommandsList.innerHTML = '';
  724. this.customCommands.forEach((command, index) => {
  725. const item = document.createElement('div');
  726. item.className = 'custom-command-item';
  727. item.textContent = command;
  728. item.addEventListener('click', () => {
  729. this.customCommandInput.value = command;
  730. });
  731. this.customCommandsList.appendChild(item);
  732. });
  733. }
  734. addMessage(text, type = 'info', customTimestamp = null) {
  735. const message = document.createElement('div');
  736. message.className = `message message-${type}`;
  737. // 使用自定义时间戳或当前时间
  738. const messageTime = customTimestamp || new Date();
  739. let content = '';
  740. if (this.showTimestampCheckbox.checked) {
  741. const hours = messageTime.getHours().toString().padStart(2, '0');
  742. const minutes = messageTime.getMinutes().toString().padStart(2, '0');
  743. const seconds = messageTime.getSeconds().toString().padStart(2, '0');
  744. const milliseconds = messageTime.getMilliseconds().toString().padStart(3, '0');
  745. const timestamp = `${hours}:${minutes}:${seconds}.<span class="milliseconds">${milliseconds}</span>`;
  746. content += `<span class="timestamp">[${timestamp}]</span>`;
  747. }
  748. content += text;
  749. message.innerHTML = content;
  750. // 保存到日志记录中,使用消息的实际时间戳
  751. this.logMessages.push({
  752. timestamp: messageTime,
  753. type: type,
  754. content: text
  755. });
  756. this.terminalOutput.appendChild(message);
  757. if (this.autoScrollCheckbox.checked) {
  758. this.scrollToBottom();
  759. }
  760. }
  761. scrollToBottom() {
  762. this.terminalOutput.scrollTop = this.terminalOutput.scrollHeight;
  763. }
  764. updateTerminalPlaceholder() {
  765. if (this.isConnected) {
  766. if (this.hexModeCheckbox.checked) {
  767. this.terminalInputField.placeholder = 'HEX模式:输入十六进制数据(如:41 42 43 或 414243)后按Enter发送...';
  768. } else {
  769. this.terminalInputField.placeholder = '输入命令后点击发送或按Enter键发送...';
  770. }
  771. } else {
  772. this.terminalInputField.placeholder = '请先连接串口...';
  773. }
  774. }
  775. updateSendOptions() {
  776. const isHexMode = this.hexModeCheckbox.checked;
  777. if (isHexMode) {
  778. // HEX模式下:保存当前设置并禁用虚拟终端模式
  779. this.virtualTerminalModeOriginalValue = this.virtualTerminalModeCheckbox.checked;
  780. this.virtualTerminalModeCheckbox.disabled = true;
  781. this.virtualTerminalMode = false; // 功能上禁用,但保持复选框原始状态
  782. // 添加视觉提示
  783. const label = this.virtualTerminalModeCheckbox.parentElement;
  784. label.style.opacity = '0.5';
  785. label.title = 'HEX模式下虚拟终端功能不可用';
  786. } else {
  787. // 文本模式下:恢复虚拟终端模式
  788. this.virtualTerminalModeCheckbox.disabled = false;
  789. this.virtualTerminalModeCheckbox.checked = this.virtualTerminalModeOriginalValue;
  790. this.virtualTerminalMode = this.virtualTerminalModeOriginalValue;
  791. // 移除视觉提示
  792. const label = this.virtualTerminalModeCheckbox.parentElement;
  793. label.style.opacity = '1';
  794. label.title = '';
  795. }
  796. // 更新终端占位符
  797. this.updateTerminalPlaceholder();
  798. }
  799. clearTerminal() {
  800. if (confirm('确定要清空终端内容吗?')) {
  801. this.terminalOutput.innerHTML = '';
  802. this.logMessages = [];
  803. this.addMessage('终端已清空', 'info');
  804. }
  805. }
  806. updateConnectionStatus(connected) {
  807. if (connected) {
  808. this.connectionStatus.className = 'status-connected';
  809. this.connectionStatus.innerHTML = '<i class="fas fa-circle"></i> 已连接';
  810. this.connectBtn.disabled = true;
  811. this.disconnectBtn.disabled = false;
  812. this.terminalInputField.disabled = false;
  813. if (this.terminalSendBtn) this.terminalSendBtn.disabled = false;
  814. this.updateTerminalPlaceholder();
  815. } else {
  816. this.connectionStatus.className = 'status-disconnected';
  817. this.connectionStatus.innerHTML = '<i class="fas fa-circle"></i> 未连接';
  818. this.connectBtn.disabled = false;
  819. this.disconnectBtn.disabled = true;
  820. this.terminalInputField.disabled = true;
  821. if (this.terminalSendBtn) this.terminalSendBtn.disabled = true;
  822. this.updateTerminalPlaceholder();
  823. }
  824. }
  825. // 参数管理方法
  826. saveParameters() {
  827. const params = {
  828. baudRate: this.baudRateSelect.value,
  829. customBaudRate: this.customBaudRateInput.value,
  830. dataBits: this.dataBitsSelect.value,
  831. parity: this.paritySelect.value,
  832. stopBits: this.stopBitsSelect.value,
  833. rttAddr: this.rttAddrInput.value,
  834. rttSize: this.rttSizeInput.value,
  835. rttChannel: this.rttChannelInput.value,
  836. svAddr: this.svAddrInput.value,
  837. svSize: this.svSizeInput.value,
  838. svChannel: this.svChannelInput.value,
  839. flmPath: this.flmPathInput.value,
  840. baseAddr: this.baseAddrInput.value,
  841. ramAddr: this.ramAddrInput.value,
  842. binPath: this.binPathInput.value,
  843. binAddr: this.binAddrInput.value,
  844. ymodemFile: this.ymodemFileInput.value,
  845. customCommands: this.customCommands,
  846. virtualTerminalMode: this.virtualTerminalModeOriginalValue,
  847. processAnsiSequences: this.processAnsiSequencesCheckbox.checked
  848. };
  849. localStorage.setItem('microlinkParams', JSON.stringify(params));
  850. this.addMessage('✅ 参数已保存到本地存储', 'info');
  851. }
  852. async saveParametersToFile() {
  853. try {
  854. // 收集当前参数
  855. const params = this.getCurrentParameters();
  856. // 生成配置文件内容
  857. const configContent = this.generateConfigFileContent(params);
  858. // 检查是否支持File System Access API
  859. if ('showSaveFilePicker' in window) {
  860. await this.saveWithFilePicker(configContent);
  861. } else {
  862. // 降级到传统下载方式
  863. this.saveWithDownload(configContent);
  864. }
  865. } catch (error) {
  866. if (error.name === 'AbortError') {
  867. this.addMessage('💡 用户取消了文件保存', 'info');
  868. } else {
  869. this.addMessage(`❌ 保存参数到文件失败: ${error.message}`, 'error');
  870. }
  871. }
  872. }
  873. async saveWithFilePicker(configContent) {
  874. try {
  875. // 使用File System Access API让用户选择保存位置
  876. const fileHandle = await window.showSaveFilePicker({
  877. suggestedName: 'microlink_web_scp.txt',
  878. types: [
  879. {
  880. description: 'MicroLink配置文件',
  881. accept: {
  882. 'text/plain': ['.txt'],
  883. },
  884. },
  885. ],
  886. });
  887. // 创建可写流并写入内容
  888. const writable = await fileHandle.createWritable();
  889. await writable.write(configContent);
  890. await writable.close();
  891. this.addMessage('✅ 参数已保存到指定位置', 'info');
  892. } catch (error) {
  893. if (error.name === 'AbortError') {
  894. throw error; // 重新抛出取消错误
  895. } else {
  896. // 如果File System Access API失败,降级到下载方式
  897. this.addMessage('⚠️ 文件选择器不可用,使用下载方式保存', 'info');
  898. this.saveWithDownload(configContent);
  899. }
  900. }
  901. }
  902. saveWithDownload(configContent) {
  903. // 传统的下载方式
  904. const blob = new Blob([configContent], { type: 'text/plain;charset=utf-8' });
  905. const url = URL.createObjectURL(blob);
  906. // 创建下载链接并触发下载
  907. const a = document.createElement('a');
  908. a.href = url;
  909. a.download = 'microlink_web_scp.txt';
  910. document.body.appendChild(a);
  911. a.click();
  912. document.body.removeChild(a);
  913. // 清理URL对象
  914. URL.revokeObjectURL(url);
  915. this.addMessage('✅ 参数已保存到下载文件夹 microlink_web_scp.txt', 'info');
  916. }
  917. getCurrentParameters() {
  918. return {
  919. baudRate: this.baudRateSelect.value,
  920. customBaudRate: this.customBaudRateInput.value,
  921. dataBits: this.dataBitsSelect.value,
  922. parity: this.paritySelect.value,
  923. stopBits: this.stopBitsSelect.value,
  924. rttAddr: this.rttAddrInput.value,
  925. rttSize: this.rttSizeInput.value,
  926. rttChannel: this.rttChannelInput.value,
  927. svAddr: this.svAddrInput.value,
  928. svSize: this.svSizeInput.value,
  929. svChannel: this.svChannelInput.value,
  930. flmPath: this.flmPathInput.value,
  931. baseAddr: this.baseAddrInput.value,
  932. ramAddr: this.ramAddrInput.value,
  933. binPath: this.binPathInput.value,
  934. binAddr: this.binAddrInput.value,
  935. ymodemFile: this.ymodemFileInput.value,
  936. customCommands: this.customCommands,
  937. virtualTerminalMode: this.virtualTerminalModeOriginalValue,
  938. processAnsiSequences: this.processAnsiSequencesCheckbox.checked
  939. };
  940. }
  941. generateConfigFileContent(params) {
  942. // 生成配置文件格式的内容
  943. const lines = [
  944. '# MicroLink Web Serial Configuration Parameters',
  945. '# 串口配置',
  946. 'port=COM3',
  947. `baudrate=${params.baudRate}`,
  948. `databits=${params.dataBits}`,
  949. `parity=${params.parity === 'none' ? 'N' : params.parity === 'even' ? 'E' : 'O'}`,
  950. `stopbits=${params.stopBits}`,
  951. '',
  952. '# RTT配置',
  953. `rtt_addr=${params.rttAddr}`,
  954. `rtt_size=${params.rttSize}`,
  955. `rtt_channel=${params.rttChannel}`,
  956. '',
  957. '# SystemView配置',
  958. `systemview_addr=${params.svAddr}`,
  959. `systemview_size=${params.svSize}`,
  960. `systemview_channel=${params.svChannel}`,
  961. '',
  962. '# FLM配置',
  963. `flm_path=${params.flmPath}`,
  964. `base_addr=${params.baseAddr}`,
  965. `ram_addr=${params.ramAddr}`,
  966. '',
  967. '# 下载配置',
  968. `bin_file_path=${params.binPath}`,
  969. `bin_addr=${params.binAddr}`,
  970. '',
  971. '# 自定义命令',
  972. `custom_commands=${params.customCommands.join(';')}`,
  973. ''
  974. ];
  975. return lines.join('\n');
  976. }
  977. async loadParameters() {
  978. // 直接从HTML配置加载
  979. this.loadConfigFromHTML();
  980. // 然后检查是否有用户保存的参数覆盖
  981. const saved = localStorage.getItem('microlinkParams');
  982. if (saved) {
  983. try {
  984. const params = JSON.parse(saved);
  985. this.applyParameters(params);
  986. this.addMessage('✅ 用户保存的参数已加载并覆盖HTML配置', 'info');
  987. } catch (error) {
  988. this.addMessage('❌ 用户参数格式错误,使用HTML配置参数', 'error');
  989. }
  990. }
  991. }
  992. async loadConfigFile() {
  993. // 手动重新加载HTML配置(按钮触发)
  994. this.addMessage('🔄 手动重新加载HTML配置...', 'info');
  995. try {
  996. const configElement = document.getElementById('embedded-config');
  997. if (configElement) {
  998. const configText = configElement.textContent;
  999. const params = this.parseConfigFile(configText);
  1000. this.applyParameters(params);
  1001. this.addMessage('✅ 参数已从HTML配置重新加载', 'info');
  1002. return;
  1003. } else {
  1004. this.addMessage('❌ HTML配置元素未找到', 'error');
  1005. }
  1006. } catch (error) {
  1007. this.addMessage(`从HTML配置加载失败: ${error.message}`, 'error');
  1008. }
  1009. // 如果HTML配置失败,使用备用配置
  1010. this.addMessage('⚠️ HTML配置加载失败,使用备用配置', 'info');
  1011. this.loadEmbeddedConfig();
  1012. }
  1013. loadConfigFromHTML() {
  1014. try {
  1015. // 从HTML中的script标签读取配置
  1016. const configElement = document.getElementById('embedded-config');
  1017. if (configElement) {
  1018. const configText = configElement.textContent;
  1019. const params = this.parseConfigFile(configText);
  1020. this.applyParameters(params);
  1021. this.addMessage('✅ 参数已从HTML配置加载', 'info');
  1022. } else {
  1023. // 如果HTML中没有配置,使用硬编码的备用配置
  1024. this.loadEmbeddedConfig();
  1025. }
  1026. } catch (error) {
  1027. this.addMessage(`从HTML配置加载失败: ${error.message}`, 'error');
  1028. this.loadEmbeddedConfig();
  1029. }
  1030. }
  1031. loadEmbeddedConfig() {
  1032. // 备用的硬编码配置
  1033. const embeddedConfig = `# MicroLink Web Serial Configuration Parameters
  1034. # 串口配置
  1035. port=COM3
  1036. baudrate=115200
  1037. databits=8
  1038. parity=N
  1039. stopbits=1
  1040. # RTT配置
  1041. rtt_addr=0x20000000
  1042. rtt_size=0x4000
  1043. rtt_channel=0
  1044. # SystemView配置
  1045. systemview_addr=0x20000000
  1046. systemview_size=0x4000
  1047. systemview_channel=1
  1048. # FLM配置
  1049. flm_path=STM32/STM32F4xx_1024.FLM.o
  1050. base_addr=0x08000000
  1051. ram_addr=0x20000000
  1052. # 下载配置
  1053. bin_file_path=firmware.bin
  1054. bin_addr=0x08000000
  1055. # 自定义命令
  1056. custom_commands=RTTView.start(0x20000000,1024,0);SystemView.start(0x20000000,1024,1);load.offline()`;
  1057. const params = this.parseConfigFile(embeddedConfig);
  1058. this.applyParameters(params);
  1059. this.addMessage('参数已从备用配置加载', 'info');
  1060. }
  1061. handleConfigFileSelect(event) {
  1062. const file = event.target.files[0];
  1063. if (file) {
  1064. const reader = new FileReader();
  1065. reader.onload = (e) => {
  1066. try {
  1067. const configText = e.target.result;
  1068. const params = this.parseConfigFile(configText);
  1069. this.applyParameters(params);
  1070. this.addMessage(`参数已从文件 "${file.name}" 加载`, 'info');
  1071. } catch (error) {
  1072. this.addMessage(`解析配置文件失败: ${error.message}`, 'error');
  1073. }
  1074. };
  1075. reader.readAsText(file);
  1076. }
  1077. }
  1078. parseConfigFile(configText) {
  1079. const params = {};
  1080. const lines = configText.split('\n');
  1081. for (const line of lines) {
  1082. const trimmedLine = line.trim();
  1083. if (trimmedLine && !trimmedLine.startsWith('#')) {
  1084. const [key, value] = trimmedLine.split('=');
  1085. if (key && value) {
  1086. const trimmedKey = key.trim();
  1087. const trimmedValue = value.trim();
  1088. // 映射配置文件的键到内部参数名
  1089. switch (trimmedKey) {
  1090. case 'baudrate':
  1091. params.baudRate = trimmedValue;
  1092. break;
  1093. case 'databits':
  1094. params.dataBits = trimmedValue;
  1095. break;
  1096. case 'parity':
  1097. params.parity = trimmedValue.toLowerCase() === 'n' ? 'none' :
  1098. trimmedValue.toLowerCase() === 'e' ? 'even' : 'odd';
  1099. break;
  1100. case 'stopbits':
  1101. params.stopBits = trimmedValue;
  1102. break;
  1103. case 'rtt_addr':
  1104. params.rttAddr = trimmedValue;
  1105. break;
  1106. case 'rtt_size':
  1107. params.rttSize = trimmedValue;
  1108. break;
  1109. case 'rtt_channel':
  1110. params.rttChannel = trimmedValue;
  1111. break;
  1112. case 'systemview_addr':
  1113. params.svAddr = trimmedValue;
  1114. break;
  1115. case 'systemview_size':
  1116. params.svSize = trimmedValue;
  1117. break;
  1118. case 'systemview_channel':
  1119. params.svChannel = trimmedValue;
  1120. break;
  1121. case 'flm_path':
  1122. params.flmPath = trimmedValue;
  1123. break;
  1124. case 'base_addr':
  1125. params.baseAddr = trimmedValue;
  1126. break;
  1127. case 'ram_addr':
  1128. params.ramAddr = trimmedValue;
  1129. break;
  1130. case 'bin_file_path':
  1131. params.binPath = trimmedValue;
  1132. break;
  1133. case 'bin_addr':
  1134. params.binAddr = trimmedValue;
  1135. break;
  1136. case 'custom_commands':
  1137. params.customCommands = trimmedValue.split(';').filter(cmd => cmd.trim());
  1138. break;
  1139. }
  1140. }
  1141. }
  1142. }
  1143. return params;
  1144. }
  1145. resetParameters() {
  1146. // 重置为默认值(与HTML配置保持一致)
  1147. const defaultParams = {
  1148. baudRate: '115200',
  1149. customBaudRate: '',
  1150. dataBits: '8',
  1151. parity: 'none',
  1152. stopBits: '1',
  1153. rttAddr: '0x20000000',
  1154. rttSize: '0x4000',
  1155. rttChannel: '0',
  1156. svAddr: '0x20000000',
  1157. svSize: '0x4000',
  1158. svChannel: '1',
  1159. flmPath: 'STM32/STM32F4xx_1024.FLM.o',
  1160. baseAddr: '0x08000000',
  1161. ramAddr: '0x20000000',
  1162. binPath: 'firmware.bin',
  1163. binAddr: '0x08000000',
  1164. ymodemFile: 'update.bin',
  1165. customCommands: [],
  1166. virtualTerminalMode: true,
  1167. processAnsiSequences: true
  1168. };
  1169. this.applyParameters(defaultParams);
  1170. this.addMessage('参数已重置为默认值', 'info');
  1171. }
  1172. applyParameters(params) {
  1173. this.baudRateSelect.value = params.baudRate || '115200';
  1174. this.customBaudRateInput.value = params.customBaudRate || '';
  1175. this.dataBitsSelect.value = params.dataBits || '8';
  1176. this.paritySelect.value = params.parity || 'none';
  1177. this.stopBitsSelect.value = params.stopBits || '1';
  1178. this.rttAddrInput.value = params.rttAddr || '0x20000000';
  1179. this.rttSizeInput.value = params.rttSize || '0x4000';
  1180. this.rttChannelInput.value = params.rttChannel || '0';
  1181. this.svAddrInput.value = params.svAddr || '0x20000000';
  1182. this.svSizeInput.value = params.svSize || '0x4000';
  1183. this.svChannelInput.value = params.svChannel || '1';
  1184. this.flmPathInput.value = params.flmPath || 'STM32/STM32F4xx_1024.FLM.o';
  1185. this.baseAddrInput.value = params.baseAddr || '0x08000000';
  1186. this.ramAddrInput.value = params.ramAddr || '0x20000000';
  1187. this.binPathInput.value = params.binPath || 'firmware.bin';
  1188. this.binAddrInput.value = params.binAddr || '0x08000000';
  1189. this.ymodemFileInput.value = params.ymodemFile || 'update.bin';
  1190. this.customCommands = params.customCommands || [];
  1191. this.updateCustomCommandsList();
  1192. // 应用虚拟终端配置
  1193. if (params.virtualTerminalMode !== undefined) {
  1194. this.virtualTerminalModeCheckbox.checked = params.virtualTerminalMode;
  1195. this.virtualTerminalMode = params.virtualTerminalMode;
  1196. this.virtualTerminalModeOriginalValue = params.virtualTerminalMode;
  1197. }
  1198. if (params.processAnsiSequences !== undefined) {
  1199. this.processAnsiSequencesCheckbox.checked = params.processAnsiSequences;
  1200. }
  1201. // 处理自定义波特率显示
  1202. if (this.baudRateSelect.value === 'custom') {
  1203. this.customBaudRateInput.style.display = 'block';
  1204. } else {
  1205. this.customBaudRateInput.style.display = 'none';
  1206. }
  1207. }
  1208. // 终端输入处理方法
  1209. handleTerminalInput(event) {
  1210. if (!this.isConnected) {
  1211. return; // 未连接时不处理按键
  1212. }
  1213. // 现在统一使用传统模式,不再实时发送
  1214. this.handleLocalTerminalKey(event);
  1215. }
  1216. // 虚拟终端按键处理
  1217. async handleVirtualTerminalKey(event) {
  1218. event.preventDefault();
  1219. let keySequence = null;
  1220. let shouldClearInput = false;
  1221. switch (event.key) {
  1222. case 'Enter':
  1223. keySequence = '\r\n';
  1224. this.currentLine = '';
  1225. shouldClearInput = true;
  1226. break;
  1227. case 'Tab':
  1228. keySequence = '\t';
  1229. shouldClearInput = true;
  1230. break;
  1231. case 'ArrowUp':
  1232. keySequence = '\x1b[A'; // ANSI escape sequence for up arrow
  1233. shouldClearInput = true;
  1234. break;
  1235. case 'ArrowDown':
  1236. keySequence = '\x1b[B'; // ANSI escape sequence for down arrow
  1237. shouldClearInput = true;
  1238. break;
  1239. case 'ArrowLeft':
  1240. keySequence = '\x1b[D'; // ANSI escape sequence for left arrow
  1241. shouldClearInput = true;
  1242. break;
  1243. case 'ArrowRight':
  1244. keySequence = '\x1b[C'; // ANSI escape sequence for right arrow
  1245. shouldClearInput = true;
  1246. break;
  1247. case 'Backspace':
  1248. keySequence = '\x08'; // Backspace character
  1249. if (this.currentLine.length > 0) {
  1250. this.currentLine = this.currentLine.slice(0, -1);
  1251. }
  1252. shouldClearInput = true;
  1253. break;
  1254. case 'Delete':
  1255. keySequence = '\x7f'; // DEL character
  1256. shouldClearInput = true;
  1257. break;
  1258. case 'Home':
  1259. keySequence = '\x1b[H';
  1260. shouldClearInput = true;
  1261. break;
  1262. case 'End':
  1263. keySequence = '\x1b[F';
  1264. shouldClearInput = true;
  1265. break;
  1266. case 'PageUp':
  1267. keySequence = '\x1b[5~';
  1268. shouldClearInput = true;
  1269. break;
  1270. case 'PageDown':
  1271. keySequence = '\x1b[6~';
  1272. shouldClearInput = true;
  1273. break;
  1274. case 'Escape':
  1275. keySequence = '\x1b';
  1276. shouldClearInput = true;
  1277. break;
  1278. default:
  1279. // 普通字符
  1280. if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
  1281. keySequence = event.key;
  1282. this.currentLine += event.key;
  1283. shouldClearInput = true;
  1284. }
  1285. // Ctrl组合键
  1286. else if (event.ctrlKey && event.key.length === 1) {
  1287. const char = event.key.toLowerCase();
  1288. if (char >= 'a' && char <= 'z') {
  1289. keySequence = String.fromCharCode(char.charCodeAt(0) - 96); // Ctrl+A = 0x01, etc.
  1290. shouldClearInput = true;
  1291. }
  1292. }
  1293. break;
  1294. }
  1295. if (keySequence) {
  1296. // 发送按键序列到串口
  1297. await this.sendRawData(keySequence);
  1298. // 在虚拟终端模式下,清除输入框内容,让远程设备控制显示
  1299. if (shouldClearInput) {
  1300. this.terminalInputField.value = '';
  1301. this.currentLine = '';
  1302. }
  1303. }
  1304. }
  1305. // 传统终端按键处理
  1306. handleLocalTerminalKey(event) {
  1307. switch (event.key) {
  1308. case 'Enter':
  1309. event.preventDefault();
  1310. this.executeTerminalCommand();
  1311. break;
  1312. case 'ArrowUp':
  1313. event.preventDefault();
  1314. this.navigateHistory(-1);
  1315. break;
  1316. case 'ArrowDown':
  1317. event.preventDefault();
  1318. this.navigateHistory(1);
  1319. break;
  1320. case 'Tab':
  1321. event.preventDefault();
  1322. // Tab键插入Tab字符
  1323. this.insertTabCharacter();
  1324. break;
  1325. }
  1326. }
  1327. // 发送原始数据到串口
  1328. async sendRawData(data) {
  1329. if (!this.isConnected || !this.port) {
  1330. return;
  1331. }
  1332. try {
  1333. const dataToSend = new TextEncoder().encode(data);
  1334. if (!this.writer) {
  1335. this.writer = this.port.writable.getWriter();
  1336. }
  1337. await this.writer.write(dataToSend);
  1338. } catch (error) {
  1339. this.addMessage(`发送数据失败: ${error.message}`, 'error');
  1340. }
  1341. }
  1342. // 发送HEX格式数据
  1343. async sendHexData(hexString) {
  1344. if (!this.isConnected) {
  1345. this.addMessage('请先连接串口', 'error');
  1346. return;
  1347. }
  1348. try {
  1349. // 清理输入:移除空格、换行符等
  1350. const cleanHex = hexString.replace(/[\s\r\n]/g, '');
  1351. // 验证是否为有效的十六进制字符串
  1352. if (!/^[0-9A-Fa-f]*$/.test(cleanHex)) {
  1353. this.addMessage('❌ 无效的十六进制数据,只能包含0-9和A-F字符', 'error');
  1354. return;
  1355. }
  1356. // 确保是偶数长度(每个字节需要2个十六进制字符)
  1357. const paddedHex = cleanHex.length % 2 === 0 ? cleanHex : '0' + cleanHex;
  1358. if (paddedHex.length === 0) {
  1359. this.addMessage('❌ 请输入有效的十六进制数据', 'error');
  1360. return;
  1361. }
  1362. // 转换为字节数组
  1363. const bytes = [];
  1364. for (let i = 0; i < paddedHex.length; i += 2) {
  1365. const byteValue = parseInt(paddedHex.substr(i, 2), 16);
  1366. bytes.push(byteValue);
  1367. }
  1368. // 创建Uint8Array并发送
  1369. const dataToSend = new Uint8Array(bytes);
  1370. if (!this.writer) {
  1371. this.writer = this.port.writable.getWriter();
  1372. }
  1373. await this.writer.write(dataToSend);
  1374. // 显示发送的数据(格式化为易读的HEX格式)
  1375. const formattedHex = paddedHex.toUpperCase().replace(/(.{2})/g, '$1 ').trim();
  1376. this.addMessage(`HEX发送 (${bytes.length}字节): ${formattedHex}`, 'sent');
  1377. } catch (error) {
  1378. this.addMessage(`发送HEX数据失败: ${error.message}`, 'error');
  1379. }
  1380. }
  1381. // 插入Tab字符(传统模式使用)
  1382. insertTabCharacter() {
  1383. const input = this.terminalInputField;
  1384. const start = input.selectionStart;
  1385. const end = input.selectionEnd;
  1386. const value = input.value;
  1387. const tabChar = '\t'; // 使用真实Tab字符
  1388. const newValue = value.substring(0, start) + tabChar + value.substring(end);
  1389. input.value = newValue;
  1390. // 将光标移动到Tab字符之后
  1391. const newCursorPos = start + tabChar.length;
  1392. input.setSelectionRange(newCursorPos, newCursorPos);
  1393. // 触发input事件以确保任何监听器都能收到通知
  1394. input.dispatchEvent(new Event('input', { bubbles: true }));
  1395. }
  1396. executeTerminalCommand() {
  1397. const command = this.terminalInputField.value.trim();
  1398. if (!command) return;
  1399. // 添加到命令历史
  1400. if (this.commandHistory[this.commandHistory.length - 1] !== command) {
  1401. this.commandHistory.push(command);
  1402. // 限制历史记录数量
  1403. if (this.commandHistory.length > 100) {
  1404. this.commandHistory.shift();
  1405. }
  1406. }
  1407. this.historyIndex = -1;
  1408. if (this.hexModeCheckbox.checked) {
  1409. // HEX模式:发送十六进制数据
  1410. this.sendHexData(command);
  1411. } else {
  1412. // 文本模式:发送普通命令
  1413. this.addMessage(`$ ${command}`, 'sent');
  1414. this.sendCommand(command);
  1415. }
  1416. // 清空输入框
  1417. this.terminalInputField.value = '';
  1418. }
  1419. navigateHistory(direction) {
  1420. if (this.commandHistory.length === 0) return;
  1421. if (direction === -1) {
  1422. // 向上箭头 - 显示更早的命令
  1423. if (this.historyIndex === -1) {
  1424. this.historyIndex = this.commandHistory.length - 1;
  1425. } else if (this.historyIndex > 0) {
  1426. this.historyIndex--;
  1427. }
  1428. } else if (direction === 1) {
  1429. // 向下箭头 - 显示更新的命令
  1430. if (this.historyIndex === -1) {
  1431. return;
  1432. } else if (this.historyIndex < this.commandHistory.length - 1) {
  1433. this.historyIndex++;
  1434. } else {
  1435. this.historyIndex = -1;
  1436. this.terminalInputField.value = '';
  1437. return;
  1438. }
  1439. }
  1440. if (this.historyIndex >= 0 && this.historyIndex < this.commandHistory.length) {
  1441. this.terminalInputField.value = this.commandHistory[this.historyIndex];
  1442. // 将光标移到末尾
  1443. setTimeout(() => {
  1444. this.terminalInputField.setSelectionRange(
  1445. this.terminalInputField.value.length,
  1446. this.terminalInputField.value.length
  1447. );
  1448. }, 0);
  1449. }
  1450. }
  1451. // 日志保存方法
  1452. async saveLog() {
  1453. try {
  1454. if (this.logMessages.length === 0) {
  1455. this.addMessage('没有日志内容可保存', 'info');
  1456. return;
  1457. }
  1458. const logContent = this.generateLogContent();
  1459. // 检查是否支持File System Access API
  1460. if ('showSaveFilePicker' in window) {
  1461. await this.saveLogWithFilePicker(logContent);
  1462. } else {
  1463. // 降级到传统下载方式
  1464. this.saveLogWithDownload(logContent);
  1465. }
  1466. } catch (error) {
  1467. if (error.name === 'AbortError') {
  1468. this.addMessage('💡 用户取消了日志保存', 'info');
  1469. } else {
  1470. this.addMessage(`❌ 保存日志失败: ${error.message}`, 'error');
  1471. }
  1472. }
  1473. }
  1474. generateLogContent() {
  1475. const lines = [];
  1476. lines.push('# MicroLink Web Serial Terminal 日志');
  1477. lines.push(`# 生成时间: ${new Date().toLocaleString()}`);
  1478. lines.push(`# 总计消息数: ${this.logMessages.length}`);
  1479. lines.push('# ========================================');
  1480. lines.push('');
  1481. for (const msg of this.logMessages) {
  1482. const timestamp = msg.timestamp.toLocaleString();
  1483. const typeLabel = this.getTypeLabel(msg.type);
  1484. lines.push(`[${timestamp}] ${typeLabel} ${msg.content}`);
  1485. }
  1486. lines.push('');
  1487. lines.push('# ========================================');
  1488. lines.push('# 日志结束');
  1489. return lines.join('\n');
  1490. }
  1491. getTypeLabel(type) {
  1492. const labels = {
  1493. 'sent': '[发送]',
  1494. 'received': '[接收]',
  1495. 'error': '[错误]',
  1496. 'info': '[信息]'
  1497. };
  1498. return labels[type] || '[未知]';
  1499. }
  1500. async saveLogWithFilePicker(logContent) {
  1501. try {
  1502. const now = new Date();
  1503. const dateStr = now.toISOString().slice(0, 19).replace(/[T:]/g, '-');
  1504. const suggestedName = `microlink-log-${dateStr}.txt`;
  1505. const fileHandle = await window.showSaveFilePicker({
  1506. suggestedName: suggestedName,
  1507. types: [
  1508. {
  1509. description: 'MicroLink日志文件',
  1510. accept: {
  1511. 'text/plain': ['.txt'],
  1512. },
  1513. },
  1514. ],
  1515. });
  1516. const writable = await fileHandle.createWritable();
  1517. await writable.write(logContent);
  1518. await writable.close();
  1519. this.addMessage('✅ 日志已保存到指定位置', 'info');
  1520. } catch (error) {
  1521. if (error.name === 'AbortError') {
  1522. throw error;
  1523. } else {
  1524. this.addMessage('⚠️ 文件选择器不可用,使用下载方式保存', 'info');
  1525. this.saveLogWithDownload(logContent);
  1526. }
  1527. }
  1528. }
  1529. saveLogWithDownload(logContent) {
  1530. const now = new Date();
  1531. const dateStr = now.toISOString().slice(0, 19).replace(/[T:]/g, '-');
  1532. const filename = `microlink-log-${dateStr}.txt`;
  1533. const blob = new Blob([logContent], { type: 'text/plain;charset=utf-8' });
  1534. const url = URL.createObjectURL(blob);
  1535. const a = document.createElement('a');
  1536. a.href = url;
  1537. a.download = filename;
  1538. document.body.appendChild(a);
  1539. a.click();
  1540. document.body.removeChild(a);
  1541. URL.revokeObjectURL(url);
  1542. this.addMessage(`✅ 日志已保存到下载文件夹 ${filename}`, 'info');
  1543. }
  1544. }
  1545. // ========== YMODEM协议实现(移植自add.html,适配window.microLinkTerminal.port) ==========
  1546. // CRC16校验计算 (用于YMODEM)
  1547. function calculateCRC16(data) {
  1548. let crc = 0x0000;
  1549. const polynomial = 0x1021; // CRC-16-CCITT
  1550. for (let i = 0; i < data.length; i++) {
  1551. crc ^= (data[i] << 8);
  1552. for (let j = 0; j < 8; j++) {
  1553. if (crc & 0x8000) {
  1554. crc = (crc << 1) ^ polynomial;
  1555. } else {
  1556. crc = crc << 1;
  1557. }
  1558. }
  1559. crc &= 0xFFFF;
  1560. }
  1561. return crc;
  1562. }
  1563. // 调试函数:验证YMODEM数据包格式
  1564. function debugYMODEMPacket(packet, packetType, blockNumber = 0) {
  1565. const packetInfo = {
  1566. type: packetType,
  1567. blockNumber: blockNumber,
  1568. totalLength: packet.length,
  1569. header: {
  1570. SOH: packet[0],
  1571. blockNumber: packet[1],
  1572. blockNumberComplement: packet[2]
  1573. },
  1574. dataArea: Array.from(packet.slice(3, 131)), // 128字节数据区(第4字节到第131字节)
  1575. crc: {
  1576. value: (packet[131] << 8) | packet[132]
  1577. }
  1578. };
  1579. console.log(`=== YMODEM ${packetType} 包调试信息 ===`);
  1580. console.log(`包类型: ${packetType}`);
  1581. console.log(`包号: ${blockNumber}`);
  1582. console.log(`总长度: ${packet.length} 字节`);
  1583. console.log(`帧头: SOH=${packet[0].toString(16)}, 包号=${packet[1].toString(16)}, 反码=${packet[2].toString(16)}`);
  1584. console.log(`数据区: 128字节(第4-131字节)`);
  1585. console.log(`数据区前16字节: ${packetInfo.dataArea.slice(0, 16).map(b => b.toString(16).padStart(2, '0')).join(' ')}`);
  1586. console.log(`数据区后16字节: ${packetInfo.dataArea.slice(-16).map(b => b.toString(16).padStart(2, '0')).join(' ')}`);
  1587. console.log(`CRC值: ${packetInfo.crc.value.toString(16).padStart(4, '0')}`);
  1588. // 验证协议规范
  1589. const validations = [];
  1590. // 验证帧头
  1591. if (packet[0] === 0x01) validations.push('✓ SOH正确');
  1592. else validations.push('✗ SOH错误');
  1593. if (packet[1] === blockNumber) validations.push('✓ 包号正确');
  1594. else validations.push('✗ 包号错误');
  1595. if (packet[2] === (255 - blockNumber)) validations.push('✓ 包号反码正确');
  1596. else validations.push('✗ 包号反码错误');
  1597. // 验证数据区长度
  1598. if (packetInfo.dataArea.length === 128) validations.push('✓ 数据区长度正确(128字节)');
  1599. else validations.push('✗ 数据区长度错误');
  1600. // 验证数据区内容
  1601. if (packetType === '起始帧') {
  1602. // 起始帧:文件名 + 0x00 + 文件大小 + 0x00 + 0x00填充
  1603. const hasNullTerminators = packetInfo.dataArea.includes(0x00);
  1604. if (hasNullTerminators) validations.push('✓ 包含NULL终止符');
  1605. else validations.push('✗ 缺少NULL终止符');
  1606. } else if (packetType === '数据帧') {
  1607. // 数据帧:有效数据 + 0x1A填充
  1608. const hasSubPadding = packetInfo.dataArea.slice(-10).some(b => b === 0x1A);
  1609. if (hasSubPadding) validations.push('✓ 包含SUB填充(0x1A)');
  1610. else validations.push('✗ 缺少SUB填充');
  1611. } else if (packetType === '结束帧') {
  1612. // 结束帧:全0x00
  1613. const allZeros = packetInfo.dataArea.every(b => b === 0x00);
  1614. if (allZeros) validations.push('✓ 数据区全为0x00');
  1615. else validations.push('✗ 数据区不全为0x00');
  1616. }
  1617. // 验证包长度
  1618. if (packet.length === 133) validations.push('✓ 包长度正确(133字节)');
  1619. else validations.push('✗ 包长度错误');
  1620. console.log('协议验证:', validations.join(', '));
  1621. console.log('=====================================');
  1622. return packetInfo;
  1623. }
  1624. // 修改后的数据包创建函数,全部使用CRC校验
  1625. function createYMODEMHeaderPacket(fileName, fileSize) {
  1626. const headerSize = 128;
  1627. const packetSize = headerSize + 5; // 3字节帧头 + 128字节数据区 + 2字节CRC
  1628. const header = new Uint8Array(packetSize);
  1629. // 严格按照YMODEM协议规范
  1630. header[0] = 0x01; // SOH - 起始帧固定使用SOH
  1631. header[1] = 0x00; // 包号固定为0x00
  1632. header[2] = 0xFF; // 包号反码 0xFF
  1633. // 构建文件头信息:文件名 + 0x00 + 文件大小 + 0x00
  1634. const headerInfo = `${fileName}\x00${fileSize}\x00`;
  1635. const headerBytes = new TextEncoder().encode(headerInfo);
  1636. // 复制文件头信息到数据区(从第4字节开始)
  1637. header.set(headerBytes, 3);
  1638. // 填充剩余字节为0x00(从文件信息结束到128字节数据区结束)
  1639. // 数据区范围:第4字节到第131字节(共128字节)
  1640. for (let i = headerBytes.length + 3; i < headerSize + 3; i++) {
  1641. header[i] = 0x00;
  1642. }
  1643. // CRC模式:计算CRC16
  1644. // 数据区:第4字节到第131字节(128字节)
  1645. const dataForCRC = header.slice(3, headerSize + 3);
  1646. const crc = calculateCRC16(dataForCRC);
  1647. header[headerSize + 3] = (crc >> 8) & 0xFF; // CRC高字节
  1648. header[headerSize + 4] = crc & 0xFF; // CRC低字节
  1649. // 调试输出
  1650. if (typeof console !== 'undefined' && console.log) {
  1651. debugYMODEMPacket(header, '起始帧', 0);
  1652. }
  1653. return header;
  1654. }
  1655. function createYMODEMDataPacket(data, blockNumber) {
  1656. const SOH_BLOCK_SIZE = 128; // SOH固定128字节
  1657. const packetSize = SOH_BLOCK_SIZE + 5; // 3字节帧头 + 128字节数据区 + 2字节CRC
  1658. const packet = new Uint8Array(packetSize);
  1659. // 严格按照YMODEM协议规范
  1660. packet[0] = 0x01; // SOH - 数据帧使用SOH(128字节块)
  1661. packet[1] = blockNumber; // 包号
  1662. packet[2] = 255 - blockNumber; // 包号反码
  1663. // 复制有效数据到数据区(从第4字节开始)
  1664. packet.set(data, 3);
  1665. // 关键修正:SOH帧数据区严格为128字节,有效数据不足时用0x1A填充
  1666. // 数据区范围:第4字节到第131字节(共128字节)
  1667. for (let i = data.length + 3; i < SOH_BLOCK_SIZE + 3; i++) {
  1668. packet[i] = 0x1A; // 用0x1A填充剩余字节
  1669. }
  1670. // CRC模式:计算CRC16
  1671. // 数据区:第4字节到第131字节(128字节)
  1672. const dataForCRC = packet.slice(3, SOH_BLOCK_SIZE + 3);
  1673. const crc = calculateCRC16(dataForCRC);
  1674. packet[SOH_BLOCK_SIZE + 3] = (crc >> 8) & 0xFF; // CRC高字节
  1675. packet[SOH_BLOCK_SIZE + 4] = crc & 0xFF; // CRC低字节
  1676. // 调试输出
  1677. if (typeof console !== 'undefined' && console.log) {
  1678. debugYMODEMPacket(packet, '数据帧', blockNumber);
  1679. }
  1680. return packet;
  1681. }
  1682. function createYMODEMEndPacket() {
  1683. const headerSize = 128;
  1684. const packetSize = headerSize + 5; // 3字节帧头 + 128字节数据区 + 2字节CRC
  1685. const header = new Uint8Array(packetSize);
  1686. // 严格按照YMODEM协议规范 - 结束帧
  1687. header[0] = 0x01; // SOH - 结束帧固定使用SOH
  1688. header[1] = 0x00; // 包号固定为0x00
  1689. header[2] = 0xFF; // 包号反码 0xFF
  1690. // 数据区全部填充0x00(空包)
  1691. // 数据区范围:第4字节到第131字节(共128字节)
  1692. for (let i = 3; i < headerSize + 3; i++) {
  1693. header[i] = 0x00;
  1694. }
  1695. // CRC模式:计算CRC16
  1696. // 数据区:第4字节到第131字节(128字节)
  1697. const dataForCRC = header.slice(3, headerSize + 3);
  1698. const crc = calculateCRC16(dataForCRC);
  1699. header[headerSize + 3] = (crc >> 8) & 0xFF; // CRC高字节
  1700. header[headerSize + 4] = crc & 0xFF; // CRC低字节
  1701. // 调试输出
  1702. if (typeof console !== 'undefined' && console.log) {
  1703. debugYMODEMPacket(header, '结束帧', 0);
  1704. }
  1705. return header;
  1706. }
  1707. async function waitForStartSignal(reader, writer, onLog) {
  1708. const timeout = 30000;
  1709. const startTime = Date.now();
  1710. let lastLogTime = 0;
  1711. let consecutiveChars = 0;
  1712. onLog && onLog('正在等待接收方启动信号...');
  1713. while (Date.now() - startTime < timeout) {
  1714. try {
  1715. const { value, done } = await reader.read();
  1716. if (done) break;
  1717. if (value && value.length > 0) {
  1718. for (let i = 0; i < value.length; i++) {
  1719. const byte = value[i];
  1720. if (byte === 0x15) return 'NAK';
  1721. if (byte === 0x18) throw new Error('接收方取消了传输');
  1722. if (byte === 0x43) return 'C';
  1723. if (byte === 0x06) return 'ACK';
  1724. if (byte === 0x2b) {
  1725. consecutiveChars++;
  1726. if (consecutiveChars >= 3) {
  1727. await writer.write(new Uint8Array([0x15]));
  1728. return 'PLUS_TRIGGER';
  1729. }
  1730. } else if (byte >= 0x20 && byte <= 0x7e) {
  1731. consecutiveChars++;
  1732. if (consecutiveChars >= 5) {
  1733. await writer.write(new Uint8Array([0x15]));
  1734. return 'CHAR_TRIGGER';
  1735. }
  1736. } else {
  1737. consecutiveChars = 0;
  1738. }
  1739. }
  1740. }
  1741. } catch (error) {
  1742. onLog && onLog('等待启动信号时出错: ' + error.message);
  1743. }
  1744. const currentTime = Date.now();
  1745. if (currentTime - lastLogTime > 5000) {
  1746. const elapsed = Math.round((currentTime - startTime) / 1000);
  1747. onLog && onLog(`等待中... (${elapsed}s/${timeout/1000}s)`);
  1748. lastLogTime = currentTime;
  1749. }
  1750. await new Promise(resolve => setTimeout(resolve, 100));
  1751. }
  1752. throw new Error('等待启动信号超时,请确保接收方已准备就绪');
  1753. }
  1754. async function sendYMODEMPacketWithACK(writer, reader, packet, blockNumber, onLog, maxRetries = 10, isHeaderPacket = false) {
  1755. let retries = 0;
  1756. let consecutiveErrors = 0; // 连续错误计数
  1757. while (retries < maxRetries) {
  1758. // 详细的包信息日志
  1759. const packetType = blockNumber === 0 ? (packet.slice(3).every(b => b === 0) ? '结束帧' : '起始帧') : '数据帧';
  1760. onLog && onLog(`准备发送${packetType} (包号: ${blockNumber}, 长度: ${packet.length}字节)`);
  1761. // 验证包格式
  1762. if (packet[0] !== 0x01) {
  1763. onLog && onLog(`❌ 包格式错误: SOH应为0x01,实际为0x${packet[0].toString(16)}`);
  1764. throw new Error('包格式错误: SOH不正确');
  1765. }
  1766. if (packet[1] !== blockNumber) {
  1767. onLog && onLog(`❌ 包格式错误: 包号应为${blockNumber},实际为${packet[1]}`);
  1768. throw new Error('包格式错误: 包号不正确');
  1769. }
  1770. if (packet[2] !== (255 - blockNumber)) {
  1771. onLog && onLog(`❌ 包格式错误: 包号反码应为${255 - blockNumber},实际为${packet[2]}`);
  1772. throw new Error('包格式错误: 包号反码不正确');
  1773. }
  1774. // 验证包长度
  1775. const expectedLength = 133; // CRC模式固定133字节
  1776. if (packet.length !== expectedLength) {
  1777. onLog && onLog(`❌ 包长度错误: 应为${expectedLength}字节,实际为${packet.length}字节`);
  1778. throw new Error('包长度错误');
  1779. }
  1780. try {
  1781. await writer.write(packet);
  1782. onLog && onLog(`✅ ${packetType}已发送,等待ACK...`);
  1783. // 等待ACK
  1784. await waitForACK(reader, onLog, isHeaderPacket);
  1785. onLog && onLog(`✅ ${packetType}确认成功`);
  1786. consecutiveErrors = 0; // 重置连续错误计数
  1787. return;
  1788. } catch (error) {
  1789. consecutiveErrors++;
  1790. if (error.message.includes('C字符')) {
  1791. onLog && onLog('检测到C字符,设备端请求重启传输');
  1792. throw new Error('RESTART_HEADER');
  1793. } else if (error.message.includes('CAN') || error.message.includes('取消')) {
  1794. onLog && onLog('检测到传输取消信号');
  1795. throw new Error('TRANSMISSION_CANCELLED');
  1796. } else if (error.message.includes('NAK')) {
  1797. onLog && onLog('收到NAK,数据包校验失败');
  1798. // NAK错误,继续重试
  1799. } else if (error.message.includes('超时')) {
  1800. onLog && onLog('等待ACK超时,设备端可能处理缓慢');
  1801. // 超时错误,可能是设备端处理缓慢
  1802. }
  1803. retries++;
  1804. onLog && onLog(`❌ ${packetType}确认失败,重试 ${retries}/${maxRetries} (连续错误: ${consecutiveErrors})`);
  1805. if (retries >= maxRetries) {
  1806. throw new Error(`${packetType}发送失败,超过最大重试次数`);
  1807. }
  1808. // 根据连续错误次数调整重试延时
  1809. let retryDelay = 1000; // 基础延时1秒
  1810. if (consecutiveErrors >= 3) {
  1811. retryDelay = 3000; // 连续错误较多时,增加延时到3秒
  1812. onLog && onLog(`连续错误较多,增加重试延时到${retryDelay}ms`);
  1813. } else if (consecutiveErrors >= 5) {
  1814. retryDelay = 5000; // 连续错误很多时,增加延时到5秒
  1815. onLog && onLog(`连续错误很多,增加重试延时到${retryDelay}ms`);
  1816. }
  1817. onLog && onLog(`等待${retryDelay}ms后重试...`);
  1818. await new Promise(resolve => setTimeout(resolve, retryDelay));
  1819. }
  1820. }
  1821. }
  1822. async function waitForACK(reader, onLog, isHeaderPacket = false) {
  1823. const timeout = 15000; // 增加超时时间到15秒
  1824. const startTime = Date.now();
  1825. let buffer = '';
  1826. while (Date.now() - startTime < timeout) {
  1827. try {
  1828. const { value, done } = await reader.read();
  1829. if (done) {
  1830. onLog && onLog('waitForACK: 串口流已关闭 (done=true)');
  1831. break;
  1832. }
  1833. if (value && value.length > 0) {
  1834. const text = new TextDecoder().decode(value);
  1835. buffer += text;
  1836. for (let i = 0; i < value.length; i++) {
  1837. const byte = value[i];
  1838. if (byte === 0x06) {
  1839. onLog && onLog('收到ACK确认');
  1840. return; // 成功收到ACK
  1841. }
  1842. if (byte === 0x15) {
  1843. onLog && onLog('收到NAK,传输失败');
  1844. throw new Error('收到NAK,传输失败');
  1845. }
  1846. if (byte === 0x18) {
  1847. onLog && onLog('收到CAN,传输被取消');
  1848. throw new Error('收到CAN,传输被取消');
  1849. }
  1850. if (byte === 0x43) {
  1851. if (isHeaderPacket) {
  1852. onLog && onLog('头包后收到C,协议正常,进入数据包1发送');
  1853. return; // 头包后收到C,视为正常
  1854. } else {
  1855. onLog && onLog('收到C字符,需要切换到CRC模式或重启传输');
  1856. throw new Error('收到C字符,需要切换到CRC模式');
  1857. }
  1858. }
  1859. }
  1860. if (onLog && value.length < 50) {
  1861. const hexBytes = Array.from(value).map(b => b.toString(16).padStart(2, '0')).join(' ');
  1862. onLog(`waitForACK: 收到内容: ${hexBytes}`);
  1863. }
  1864. }
  1865. } catch (error) {
  1866. if (error.message.includes('NAK') || error.message.includes('CAN') || error.message.includes('C字符')) {
  1867. throw error;
  1868. }
  1869. onLog && onLog(`waitForACK读取错误: ${error.message}`);
  1870. }
  1871. await new Promise(resolve => setTimeout(resolve, 100));
  1872. }
  1873. throw new Error('等待ACK超时');
  1874. }
  1875. async function waitForACKOrNAK(reader, onLog) {
  1876. const timeout = 10000;
  1877. const startTime = Date.now();
  1878. while (Date.now() - startTime < timeout) {
  1879. try {
  1880. const { value, done } = await reader.read();
  1881. if (done) break;
  1882. if (value && value.length > 0) {
  1883. for (let i = 0; i < value.length; i++) {
  1884. const byte = value[i];
  1885. if (byte === 0x06) return 'ACK';
  1886. if (byte === 0x15) return 'NAK';
  1887. if (byte === 0x18) throw new Error('收到CAN,传输被取消');
  1888. if (byte === 0x43) throw new Error('收到C字符,需要切换到CRC模式');
  1889. }
  1890. }
  1891. } catch (error) {
  1892. if (error.message.includes('CAN') || error.message.includes('C字符')) {
  1893. throw error;
  1894. }
  1895. }
  1896. await new Promise(resolve => setTimeout(resolve, 50));
  1897. }
  1898. throw new Error('等待ACK或NAK超时');
  1899. }
  1900. // 发送EOT并等待ACK/NAK/C/CAN,支持重试
  1901. async function sendEOTWithACKRetry(writer, reader, config, onLog) {
  1902. const eotTimeout = 1000; // EOT等待ACK最大1秒
  1903. const maxEOTRetries = 5;
  1904. for (let retry = 0; retry < maxEOTRetries; retry++) {
  1905. onLog && onLog(`[DEBUG] 第${retry+1}次发送EOT信号...`);
  1906. await writer.write(new Uint8Array([0x04]));
  1907. onLog && onLog('发送EOT信号,等待ACK...');
  1908. try {
  1909. const resp = await waitForACKorC(reader, onLog, eotTimeout);
  1910. if (resp === 'ACK') {
  1911. onLog && onLog('EOT确认成功');
  1912. return true;
  1913. }
  1914. if (resp === 'C') throw new Error('RESTART_HEADER');
  1915. } catch (e) {
  1916. if (e.message === 'RESTART_HEADER') throw e;
  1917. onLog && onLog(`EOT未确认,重试${retry + 1}/${maxEOTRetries}`);
  1918. await new Promise(r => setTimeout(r, config.retryDelay + retry * 200));
  1919. }
  1920. }
  1921. onLog && onLog('❌ EOT多次重试失败,设备端未响应ACK,传输中止。');
  1922. throw new Error('EOT多次重试失败');
  1923. }
  1924. // EOT发送函数:发送EOT后等待ACK,支持重试
  1925. async function sendEOT(writer, reader, onLog, maxRetries = 5) {
  1926. for (let retry = 0; retry < maxRetries; retry++) {
  1927. onLog && onLog(`[DEBUG] 第${retry+1}次发送EOT信号...`);
  1928. await writer.write(new Uint8Array([0x04]));
  1929. onLog && onLog('发送EOT信号,等待ACK...');
  1930. try {
  1931. const resp = await waitForACKorC(reader, onLog, 1000); // 1秒超时
  1932. if (resp === 'ACK') {
  1933. onLog && onLog('EOT确认成功');
  1934. return true;
  1935. }
  1936. if (resp === 'C') throw new Error('RESTART_HEADER');
  1937. } catch (e) {
  1938. if (e.message === 'RESTART_HEADER') throw e;
  1939. onLog && onLog(`EOT未确认,重试${retry + 1}/${maxRetries}`);
  1940. await new Promise(r => setTimeout(r, 200 + retry * 100));
  1941. }
  1942. }
  1943. onLog && onLog('❌ EOT多次重试失败,设备端未响应ACK,传输中止。');
  1944. throw new Error('EOT多次重试失败');
  1945. }
  1946. // waitForACKorC函数,支持自定义超时
  1947. async function waitForACKorC(reader, onLog, timeout = 1000) {
  1948. const startTime = Date.now();
  1949. while (Date.now() - startTime < timeout) {
  1950. try {
  1951. const { value, done } = await reader.read();
  1952. if (done) break;
  1953. if (value && value.length > 0) {
  1954. for (let i = 0; i < value.length; i++) {
  1955. const byte = value[i];
  1956. if (byte === 0x06) return 'ACK';
  1957. if (byte === 0x15) return 'NAK';
  1958. if (byte === 0x18) throw new Error('收到CAN,传输被取消');
  1959. if (byte === 0x43) return 'C';
  1960. }
  1961. }
  1962. } catch (error) {
  1963. if (error.message.includes('CAN') || error.message.includes('C字符')) {
  1964. throw error;
  1965. }
  1966. }
  1967. await new Promise(resolve => setTimeout(resolve, 50));
  1968. }
  1969. throw new Error('等待ACK或C超时');
  1970. }
  1971. // 主入口:window.ymodemSendFileViaSerial
  1972. window.ymodemSendFileViaSerial = async function(uint8Array, fileName, timeout, onProgress, onLog, options = {}) {
  1973. const port = window.microLinkTerminal && window.microLinkTerminal.port;
  1974. if (!port) throw new Error('串口未连接');
  1975. const config = {
  1976. retryDelay: options.retryDelay || 1000,
  1977. maxRetries: options.maxRetries || 20,
  1978. packetTimeout: options.packetTimeout || 15000,
  1979. restartDelay: options.restartDelay || 2000,
  1980. packetInterval: options.packetInterval || 1000,
  1981. ...options
  1982. };
  1983. const SOH_BLOCK_SIZE = 128;
  1984. let writer = null, reader = null;
  1985. let restartCount = 0;
  1986. const maxRestarts = 3;
  1987. // === 头包构造函数优先用options.buildHeaderPacket ===
  1988. const buildHeaderPacket = options.buildHeaderPacket || buildHeaderPacketYmodem;
  1989. try {
  1990. writer = port.writable.getWriter();
  1991. reader = port.readable.getReader();
  1992. onLog && onLog(`准备发送文件: ${fileName}, 大小: ${uint8Array.length} 字节`);
  1993. onLog && onLog(`数据包大小: ${SOH_BLOCK_SIZE} 字节 (SOH)`);
  1994. onLog && onLog(`预计数据包数量: ${Math.ceil(uint8Array.length / SOH_BLOCK_SIZE)}`);
  1995. onLog && onLog(`包间延时: ${config.packetInterval}ms`);
  1996. onLog && onLog(`包超时: ${config.packetTimeout}ms`);
  1997. onLog && onLog(`校验方式: CRC16 (固定)`);
  1998. await new Promise(r => setTimeout(r, 1000));
  1999. await writer.write(new Uint8Array([0x00]));
  2000. await new Promise(r => setTimeout(r, 200));
  2001. await writer.write(new Uint8Array([0x43]));
  2002. await new Promise(r => setTimeout(r, 200));
  2003. onLog && onLog('已发送触发字符序列,等待接收方C信号...');
  2004. while (restartCount <= maxRestarts) {
  2005. try {
  2006. await window.waitForCSignal(reader, onLog);
  2007. await performYMODEMTransfer();
  2008. onLog && onLog('YMODEM传输成功完成!');
  2009. return;
  2010. } catch (error) {
  2011. // === 修正:EOT后收到C时直接发空头包,不再整体重启 ===
  2012. if (error.message === 'RESTART_HEADER' && restartCount < maxRestarts) {
  2013. onLog && onLog('EOT后收到C,直接发送空头包...');
  2014. let endPacket = createYMODEMEndPacket();
  2015. const endAck = await sendEndPacketWithRetry(writer, reader, endPacket, onLog, 10);
  2016. if (!endAck) throw new Error('空头包多次重试失败,传输中止');
  2017. return;
  2018. } else if (error.message === 'RESTART_HEADER') {
  2019. throw new Error('EOT后收到C,结束帧多次失败,传输中止');
  2020. } else if (error.message === 'TRANSMISSION_CANCELLED') {
  2021. throw new Error('传输被取消');
  2022. } else {
  2023. throw error;
  2024. }
  2025. }
  2026. }
  2027. throw new Error(`传输失败,已尝试${maxRestarts}次重启`);
  2028. // 内部传输函数
  2029. async function performYMODEMTransfer() {
  2030. onLog && onLog('=== 开始YMODEM传输流程 ===');
  2031. // 1. 发送起始帧
  2032. let headerPacket = buildHeaderPacket(fileName, uint8Array.length);
  2033. await sendPacketWithACKRetry(writer, reader, headerPacket, 0, config, onLog, '起始帧');
  2034. onLog && onLog(`[DEBUG] 头包ACK后,准备延时${config.packetInterval}ms`);
  2035. await new Promise(r => setTimeout(r, config.packetInterval)); // 头包和数据包1之间加延时
  2036. onLog && onLog(`[DEBUG] 延时结束,准备发送数据包1`);
  2037. // 2. 发送数据帧
  2038. const totalBlocks = Math.ceil(uint8Array.length / SOH_BLOCK_SIZE);
  2039. let transferred = 0;
  2040. for (let blockNum = 1; blockNum <= totalBlocks; blockNum++) {
  2041. const startIndex = (blockNum - 1) * SOH_BLOCK_SIZE;
  2042. const endIndex = Math.min(startIndex + SOH_BLOCK_SIZE, uint8Array.length);
  2043. const blockData = uint8Array.slice(startIndex, endIndex);
  2044. // === 强制用buildYmodemPacketYmodem构造数据包 ===
  2045. let dataPacket = buildYmodemPacketYmodem(blockNum, blockData);
  2046. await sendPacketWithACKRetry(writer, reader, dataPacket, blockNum, config, onLog, `数据帧${blockNum}`);
  2047. transferred += blockData.length;
  2048. const progress = Math.round((transferred / uint8Array.length) * 100);
  2049. onProgress && onProgress(progress);
  2050. onLog && onLog(`✅ 数据帧${blockNum}/${totalBlocks} 传输完成 (${progress}%)`);
  2051. onLog && onLog(`[DEBUG] 数据包${blockNum} ACK后,准备延时${config.packetInterval}ms`);
  2052. await new Promise(r => setTimeout(r, config.packetInterval)); // 包间延时
  2053. onLog && onLog(`[DEBUG] 延时结束,准备发送下一个数据包`);
  2054. }
  2055. // 3. 发送EOT,等待ACK
  2056. await sendEOTWithACKRetry(writer, reader, config, onLog);
  2057. await new Promise(r => setTimeout(r, config.packetInterval)); // 包间延时
  2058. // === 关键:EOT和空头包之间增加延时 ===
  2059. await new Promise(r => setTimeout(r, 500));
  2060. // 4. 发送结束帧(空头包)
  2061. let endPacket = createYMODEMEndPacket();
  2062. const endAck = await sendEndPacketWithRetry(writer, reader, endPacket, onLog, 10);
  2063. if (!endAck) throw new Error('空头包多次重试失败,传输中止');
  2064. await new Promise(r => setTimeout(r, config.packetInterval)); // 包间延时
  2065. // === 关键:结束包后严格等待ACK ===
  2066. onLog && onLog('等待设备端ACK确认结束...');
  2067. let gotAck = false;
  2068. const ackStart = Date.now();
  2069. while (Date.now() - ackStart < 5000) { // 最多等5秒
  2070. const { value, done } = await Promise.race([
  2071. reader.read(),
  2072. new Promise(resolve => setTimeout(() => resolve({value: null, done: false}), 200))
  2073. ]);
  2074. if (value) {
  2075. for (let i = 0; i < value.length; i++) {
  2076. if (value[i] === 0x06) { // ACK
  2077. gotAck = true;
  2078. onLog('✅ 设备端ACK确认,YMODEM流程完成');
  2079. break;
  2080. }
  2081. }
  2082. if (gotAck) break;
  2083. const text = new TextDecoder().decode(value);
  2084. onLog('结束后收到内容: ' + text);
  2085. }
  2086. await new Promise(r => setTimeout(r, 100));
  2087. }
  2088. if (!gotAck) onLog('⚠️ 结束后未收到ACK,可能已完成也可能异常');
  2089. onLog && onLog('✅ YMODEM传输流程完成');
  2090. }
  2091. } finally {
  2092. if (writer) try { writer.releaseLock(); } catch (e) {}
  2093. if (reader) try { reader.releaseLock(); } catch (e) {}
  2094. }
  2095. };
  2096. // 简化的YMODEM发送函数,专门处理设备端问题
  2097. window.ymodemSendFileViaSerialSimple = async function(uint8Array, fileName, timeout, onProgress, onLog, options = {}) {
  2098. const port = window.microLinkTerminal && window.microLinkTerminal.port;
  2099. if (!port) throw new Error('串口未连接');
  2100. let writer = null, reader = null;
  2101. // === 1. 先彻底暂停主终端监听 ===
  2102. let wasConnected = false;
  2103. window.isYmodemActive = true; // YMODEM流程期间屏蔽主终端
  2104. if (window.microLinkTerminal) {
  2105. // --- YMODEM前清空主终端缓冲 ---
  2106. if (typeof window.microLinkTerminal.flushBuffer === 'function') {
  2107. window.microLinkTerminal.flushBuffer();
  2108. }
  2109. wasConnected = window.microLinkTerminal.isConnected;
  2110. window.microLinkTerminal.isConnected = false;
  2111. if (window.microLinkTerminal.reader) {
  2112. try { await window.microLinkTerminal.reader.cancel(); } catch (e) {}
  2113. try { window.microLinkTerminal.reader.releaseLock(); } catch (e) {}
  2114. window.microLinkTerminal.reader = null;
  2115. }
  2116. // 等待主终端读取循环彻底退出
  2117. await new Promise(r => setTimeout(r, 300));
  2118. }
  2119. // === 头包构造函数优先用options.buildHeaderPacket ===
  2120. const buildHeaderPacket = options.buildHeaderPacket || buildHeaderPacketYmodem;
  2121. try {
  2122. // === 2. 再获取writer/reader并发送ym.receive()指令 ===
  2123. writer = port.writable.getWriter();
  2124. reader = port.readable.getReader();
  2125. onLog && onLog('发送 ym.receive() 指令...');
  2126. await writer.write(new TextEncoder().encode('ym.receive()\n'));
  2127. // 2. 等待接收端发送 'C'
  2128. onLog && onLog('等待接收端发送 "C"...');
  2129. if (!(await waitForByteYmodem(reader, 0x43, 10000, onLog))) {
  2130. onLog && onLog('未收到接收端 "C",发送中止');
  2131. throw new Error('未收到接收端 "C"');
  2132. }
  2133. // 3. 发送头包
  2134. onLog && onLog('发送 Ymodem 文件头包...');
  2135. if (!(await sendAndWaitAckYmodem(writer, reader, buildHeaderPacket(fileName, uint8Array.length), onLog))) {
  2136. onLog && onLog('头包发送失败,发送中止');
  2137. throw new Error('头包发送失败');
  2138. }
  2139. // 4. 等待接收端再次发送 'C'
  2140. onLog && onLog('等待接收端再次发送 "C"...');
  2141. if (!(await waitForByteYmodem(reader, 0x43, 10000, onLog))) {
  2142. onLog && onLog('未收到接收端第二个 "C",发送中止');
  2143. throw new Error('未收到接收端第二个 "C"');
  2144. }
  2145. // 5. 发送数据包
  2146. let seq = 1;
  2147. for (let offset = 0; offset < uint8Array.length; offset += 128) {
  2148. let chunk = uint8Array.slice(offset, offset + 128);
  2149. // === 强制用buildYmodemPacketYmodem构造数据包 ===
  2150. if (!(await sendAndWaitAckYmodem(writer, reader, buildYmodemPacketYmodem(seq, chunk), onLog))) {
  2151. onLog && onLog(`数据包 #${seq} 发送失败,发送中止`);
  2152. throw new Error(`数据包 #${seq} 发送失败`);
  2153. }
  2154. seq++;
  2155. if (onProgress) onProgress(Math.round((offset + chunk.length) / uint8Array.length * 100));
  2156. }
  2157. // 6. 发送EOT,等待ACK
  2158. onLog && onLog('发送 EOT...');
  2159. for (let i = 0; i < 10; i++) {
  2160. await writer.write(new Uint8Array([0x04]));
  2161. let b = await readByteYmodem(reader, 3000, onLog);
  2162. if (b === 0x06) break;
  2163. }
  2164. // 7. 等待接收端发送 'C'
  2165. onLog && onLog('等待接收端发送 "C"...');
  2166. if (!(await waitForByteYmodem(reader, 0x43, 10000, onLog))) {
  2167. onLog && onLog('未收到接收端最后一个 "C",发送中止');
  2168. throw new Error('未收到接收端最后一个 "C"');
  2169. }
  2170. // 8. 发送空头包
  2171. onLog && onLog('发送空头包...');
  2172. if (!(await sendEndPacketWithRetry(writer, reader, buildEndPacketYmodem(), onLog, 10))) {
  2173. onLog && onLog('空头包发送失败');
  2174. throw new Error('空头包发送失败');
  2175. }
  2176. onLog && onLog('文件发送完成!');
  2177. } finally {
  2178. if (writer) try { writer.releaseLock(); } catch (e) {}
  2179. if (reader) try { reader.releaseLock(); } catch (e) {}
  2180. // === 恢复主终端监听 ===
  2181. if (window.microLinkTerminal) {
  2182. window.microLinkTerminal.isConnected = wasConnected;
  2183. if (wasConnected) window.microLinkTerminal.startReading();
  2184. }
  2185. window.isYmodemActive = false; // 恢复主终端
  2186. }
  2187. };
  2188. function crc16_ccitt(buf) {
  2189. let crc = 0x0000;
  2190. for (let i = 0; i < buf.length; i++) {
  2191. crc ^= (buf[i] << 8);
  2192. for (let j = 0; j < 8; j++) {
  2193. if (crc & 0x8000) {
  2194. crc = (crc << 1) ^ 0x1021;
  2195. } else {
  2196. crc = crc << 1;
  2197. }
  2198. crc &= 0xFFFF;
  2199. }
  2200. }
  2201. return crc;
  2202. }
  2203. function buildYmodemPacketYmodem(seq, data) {
  2204. let packet = new Uint8Array(133);
  2205. packet[0] = 0x01; // SOH
  2206. packet[1] = seq & 0xFF;
  2207. packet[2] = (~seq) & 0xFF;
  2208. for (let i = 0; i < 128; i++) {
  2209. packet[3 + i] = data[i] !== undefined ? data[i] : 0x1A;
  2210. }
  2211. let crc = crc16_ccitt(packet.slice(3, 131));
  2212. packet[131] = (crc >> 8) & 0xFF;
  2213. packet[132] = crc & 0xFF;
  2214. return packet;
  2215. }
  2216. // === 极简YMODEM头包构造(前缀逻辑保持不变,外部传入name) ===
  2217. function buildHeaderPacketYmodem(name, size) {
  2218. // 检查是否为FLM文件,如果是则自动加前缀
  2219. if (name && !name.startsWith('FLM/') && name.endsWith('.FLM.o')) {
  2220. name = 'FLM/' + name;
  2221. }
  2222. let data = new Uint8Array(128);
  2223. let nameBytes = new TextEncoder().encode(name);
  2224. data.set(nameBytes, 0);
  2225. let sizeBytes = new TextEncoder().encode(size.toString());
  2226. data.set(sizeBytes, nameBytes.length + 1);
  2227. return buildYmodemPacketYmodem(0, data);
  2228. }
  2229. // === 极简YMODEM结束包构造 ===
  2230. function buildEndPacketYmodem() {
  2231. let data = new Uint8Array(128);
  2232. return buildYmodemPacketYmodem(0, data);
  2233. }
  2234. // === 极简YMODEM主流程(可被各tab直接调用) ===
  2235. window.sendFileViaYmodem = async function(port, fileBuffer, fileName, fileSize, onLog) {
  2236. let writer = port.writable.getWriter();
  2237. let reader = port.readable.getReader();
  2238. function log(msg) { onLog && onLog(msg); }
  2239. // 1. 发送ym.receive()
  2240. log('发送 ym.receive() 指令...');
  2241. await writer.write(new TextEncoder().encode('ym.receive()\n'));
  2242. // 2. 等待C
  2243. log('等待接收端发送 "C"...');
  2244. if (!(await waitForByteYmodem(reader, 0x43, 10000, log))) {
  2245. log('未收到接收端 "C",发送中止');
  2246. return false;
  2247. }
  2248. // 3. 发送头包
  2249. log('发送 Ymodem 文件头包...');
  2250. if (!(await sendAndWaitAckYmodem(writer, reader, buildHeaderPacketYmodem(fileName, fileSize), log))) {
  2251. log('头包发送失败,发送中止');
  2252. return false;
  2253. }
  2254. // 4. 等待C
  2255. log('等待接收端再次发送 "C"...');
  2256. if (!(await waitForByteYmodem(reader, 0x43, 10000, log))) {
  2257. log('未收到接收端第二个 "C",发送中止');
  2258. return false;
  2259. }
  2260. // 5. 发送数据包
  2261. let seq = 1;
  2262. for (let offset = 0; offset < fileBuffer.length; offset += 128) {
  2263. let chunk = fileBuffer.slice(offset, offset + 128);
  2264. log(`发送数据包 #${seq}...`);
  2265. if (!(await sendAndWaitAckYmodem(writer, reader, buildYmodemPacketYmodem(seq, chunk), log))) {
  2266. log(`数据包 #${seq} 发送失败,发送中止`);
  2267. return false;
  2268. }
  2269. seq++;
  2270. }
  2271. // 6. 发送EOT
  2272. log('发送 EOT...');
  2273. for (let i = 0; i < 10; i++) {
  2274. await writer.write(new Uint8Array([0x04]));
  2275. let b = await readByteYmodem(reader, 3000, log);
  2276. if (b === 0x06) break;
  2277. }
  2278. // 7. 等待C
  2279. log('等待接收端发送 "C"...');
  2280. if (!(await waitForByteYmodem(reader, 0x43, 10000, log))) {
  2281. log('未收到接收端最后一个 "C",发送中止');
  2282. return false;
  2283. }
  2284. // 8. 发送空头包
  2285. log('发送空头包...');
  2286. if (!(await sendEndPacketWithRetry(writer, reader, buildEndPacketYmodem(), log))) {
  2287. log('空头包发送失败');
  2288. return false;
  2289. }
  2290. log('文件发送完成!');
  2291. writer.releaseLock();
  2292. reader.releaseLock();
  2293. return true;
  2294. }
  2295. async function readByteYmodem(reader, timeout = 3000, onLog) {
  2296. const timer = setTimeout(() => reader.cancel(), timeout);
  2297. try {
  2298. const { value } = await reader.read();
  2299. clearTimeout(timer);
  2300. return value ? value[0] : null;
  2301. } catch {
  2302. clearTimeout(timer);
  2303. return null;
  2304. }
  2305. }
  2306. async function waitForByteYmodem(reader, target, timeout = 10000, onLog) {
  2307. let start = Date.now();
  2308. while (Date.now() - start < timeout) {
  2309. let b = await readByteYmodem(reader, timeout, onLog);
  2310. if (b === target) return true;
  2311. }
  2312. return false;
  2313. }
  2314. async function sendAndWaitAckYmodem(writer, reader, packet, onLog, retry = 10) {
  2315. for (let i = 0; i < retry; i++) {
  2316. await writer.write(packet);
  2317. let b = await readByteYmodem(reader, 3000, onLog);
  2318. if (b === 0x06) return true; // ACK
  2319. if (b === 0x15) continue; // NAK
  2320. }
  2321. return false;
  2322. }
  2323. // 测试函数:验证YMODEM数据包格式
  2324. window.testYMODEMPacketFormat = function() {
  2325. console.log('=== YMODEM数据包格式测试(CRC模式) ===');
  2326. // 测试起始帧
  2327. console.log('\n1. 测试起始帧格式');
  2328. const headerPacket = createYMODEMHeaderPacket('test.bin', 1024);
  2329. debugYMODEMPacket(headerPacket, '起始帧', 0);
  2330. // 验证起始帧长度
  2331. console.log(`起始帧长度验证: ${headerPacket.length}字节 (期望: 133字节)`);
  2332. console.log(`数据区长度验证: ${headerPacket.slice(3, 131).length}字节 (期望: 128字节)`);
  2333. // 测试数据帧 - 完整数据
  2334. console.log('\n2. 测试数据帧格式(完整数据)');
  2335. const fullData = new Uint8Array(128);
  2336. for (let i = 0; i < 128; i++) {
  2337. fullData[i] = i;
  2338. }
  2339. const fullDataPacket = createYMODEMDataPacket(fullData, 1);
  2340. debugYMODEMPacket(fullDataPacket, '数据帧(完整)', 1);
  2341. // 测试数据帧 - 部分数据(需要填充)
  2342. console.log('\n3. 测试数据帧格式(部分数据,需要0x1A填充)');
  2343. const partialData = new Uint8Array(64);
  2344. for (let i = 0; i < 64; i++) {
  2345. partialData[i] = i;
  2346. }
  2347. const partialDataPacket = createYMODEMDataPacket(partialData, 2);
  2348. debugYMODEMPacket(partialDataPacket, '数据帧(部分)', 2);
  2349. // 验证填充逻辑
  2350. const dataArea = partialDataPacket.slice(3, 131);
  2351. const originalData = dataArea.slice(0, 64);
  2352. const paddingData = dataArea.slice(64);
  2353. console.log(`原始数据长度: ${originalData.length}字节`);
  2354. console.log(`填充数据长度: ${paddingData.length}字节`);
  2355. console.log(`填充数据是否全为0x1A: ${paddingData.every(b => b === 0x1A)}`);
  2356. // 测试结束帧
  2357. console.log('\n4. 测试结束帧格式');
  2358. const endPacket = createYMODEMEndPacket();
  2359. debugYMODEMPacket(endPacket, '结束帧', 0);
  2360. // 验证结束帧数据区
  2361. const endDataArea = endPacket.slice(3, 131);
  2362. console.log(`结束帧数据区是否全为0x00: ${endDataArea.every(b => b === 0x00)}`);
  2363. console.log('\n=== 测试完成 ===');
  2364. console.log('关键验证点:');
  2365. console.log('- 所有SOH帧数据区严格为128字节');
  2366. console.log('- 数据帧不足128字节时用0x1A填充');
  2367. console.log('- 所有包使用CRC16校验');
  2368. console.log('- 所有包长度133字节');
  2369. return true;
  2370. };
  2371. // 页面加载完成后初始化
  2372. document.addEventListener('DOMContentLoaded', () => {
  2373. window.microLinkTerminal = new MicroLinkTerminal();
  2374. // 在开发模式下自动运行测试
  2375. if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
  2376. console.log('开发模式:运行YMODEM数据包格式测试');
  2377. setTimeout(() => {
  2378. window.testYMODEMPacketFormat();
  2379. }, 1000);
  2380. }
  2381. });
  2382. // --- handleReceivedData 屏蔽逻辑 ---
  2383. const origHandleReceivedData = MicroLinkTerminal.prototype.handleReceivedData;
  2384. MicroLinkTerminal.prototype.handleReceivedData = function(data) {
  2385. if (window.isYmodemActive) return;
  2386. // 自动接入变量分析曲线
  2387. // 旧的handleRealtimeHexData已删除,现在使用Worker方式
  2388. return origHandleReceivedData.call(this, data);
  2389. };
  2390. // --- handleReceivedData 屏蔽逻辑 ---
  2391. // --- flushBuffer 屏蔽逻辑 ---
  2392. const origFlushBuffer = MicroLinkTerminal.prototype.flushBuffer;
  2393. MicroLinkTerminal.prototype.flushBuffer = function() {
  2394. if (window.isYmodemActive) return;
  2395. return origFlushBuffer.call(this);
  2396. };
  2397. // --- flushBuffer 屏蔽逻辑 ---
  2398. // --- startReading 屏蔽逻辑 ---
  2399. const origStartReading = MicroLinkTerminal.prototype.startReading;
  2400. MicroLinkTerminal.prototype.startReading = function() {
  2401. if (window.isYmodemActive) return; // YMODEM期间禁止重启监听
  2402. return origStartReading.call(this);
  2403. };
  2404. // --- startReading 屏蔽逻辑 ---
  2405. function waitForCSignal(reader, onLog) {
  2406. // ...原有实现...
  2407. }
  2408. window.waitForCSignal = waitForCSignal;
  2409. function sendPacketWithACKRetry(writer, reader, packet, blockNumber, config, onLog, packetType, isHeaderPacket) {
  2410. // ...原有实现...
  2411. }
  2412. window.sendPacketWithACKRetry = sendPacketWithACKRetry;
  2413. // === 关键:结束包后严格等待ACK,超时重发,最多10次 ===
  2414. async function sendEndPacketWithRetry(writer, reader, endPacket, onLog, maxRetry = 10) {
  2415. for (let i = 0; i < maxRetry; i++) {
  2416. await writer.write(endPacket);
  2417. onLog && onLog(`发送空头包(第${i+1}次),等待ACK...`);
  2418. let gotAck = false;
  2419. const ackStart = Date.now();
  2420. while (Date.now() - ackStart < 5000) { // 最多等5秒
  2421. const { value, done } = await Promise.race([
  2422. reader.read(),
  2423. new Promise(resolve => setTimeout(() => resolve({value: null, done: false}), 200))
  2424. ]);
  2425. if (value) {
  2426. for (let j = 0; j < value.length; j++) {
  2427. if (value[j] === 0x06) { // ACK
  2428. gotAck = true;
  2429. onLog && onLog('✅ 设备端ACK确认,YMODEM流程完成');
  2430. return true;
  2431. }
  2432. }
  2433. }
  2434. await new Promise(r => setTimeout(r, 100));
  2435. }
  2436. onLog && onLog(`⚠️ 空头包第${i+1}次未收到ACK,准备重发...`);
  2437. }
  2438. onLog && onLog('❌ 空头包多次重试失败,设备端未响应ACK,传输中止。');
  2439. return false;
  2440. }
  2441. // 多文件配置功能
  2442. let fileTableBody, addFileBtn, clearFilesBtn;
  2443. // 确保DOM加载完成后再初始化
  2444. function initMultiFileConfig() {
  2445. fileTableBody = document.getElementById('fileTableBody');
  2446. addFileBtn = document.getElementById('addFileBtn');
  2447. clearFilesBtn = document.getElementById('clearFilesBtn');
  2448. if (!fileTableBody || !addFileBtn || !clearFilesBtn) {
  2449. console.error('多文件配置元素未找到,延迟初始化...');
  2450. setTimeout(initMultiFileConfig, 100);
  2451. return;
  2452. }
  2453. // 初始化表格
  2454. initFileTable();
  2455. // 事件监听器
  2456. addFileBtn.addEventListener('click', function() {
  2457. console.log('添加文件按钮被点击');
  2458. addFileRow();
  2459. if (window.updateCodePreview) {
  2460. window.updateCodePreview();
  2461. }
  2462. });
  2463. // 测试按钮是否正常工作
  2464. console.log('多文件配置初始化完成', {
  2465. fileTableBody: !!fileTableBody,
  2466. addFileBtn: !!addFileBtn,
  2467. clearFilesBtn: !!clearFilesBtn
  2468. });
  2469. clearFilesBtn.addEventListener('click', function() {
  2470. if (confirm('确定要清空所有文件配置吗?')) {
  2471. clearAllFiles();
  2472. }
  2473. });
  2474. }
  2475. // 初始化文件表格
  2476. function initFileTable() {
  2477. if (!fileTableBody) return;
  2478. fileTableBody.innerHTML = '';
  2479. if (window.config && window.config.files) {
  2480. window.config.files.forEach((file, index) => {
  2481. addFileRow(file, index);
  2482. });
  2483. }
  2484. }
  2485. // 添加文件行
  2486. function addFileRow(file = null, index = null) {
  2487. console.log('添加文件行被调用', { file, index });
  2488. if (!fileTableBody) {
  2489. console.error('fileTableBody 未找到');
  2490. return;
  2491. }
  2492. const row = document.createElement('div');
  2493. row.className = 'file-table-row';
  2494. row.dataset.index = index !== null ? index : (window.config && window.config.files ? window.config.files.length : 0);
  2495. const fileNameInput = document.createElement('input');
  2496. fileNameInput.type = 'text';
  2497. fileNameInput.placeholder = '例如: boot.bin';
  2498. fileNameInput.value = file ? file.fileName : '';
  2499. const addressInput = document.createElement('input');
  2500. addressInput.type = 'text';
  2501. addressInput.placeholder = '例如: 0x08000000';
  2502. addressInput.value = file ? file.address : '';
  2503. const algorithmInput = document.createElement('input');
  2504. algorithmInput.type = 'text';
  2505. algorithmInput.placeholder = '例如: STM32F7x_1024.FLM.o';
  2506. algorithmInput.value = file ? file.algorithm : '';
  2507. const deleteBtn = document.createElement('button');
  2508. deleteBtn.className = 'delete-file-btn';
  2509. deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
  2510. deleteBtn.title = '删除此行';
  2511. row.appendChild(fileNameInput);
  2512. row.appendChild(addressInput);
  2513. row.appendChild(algorithmInput);
  2514. row.appendChild(deleteBtn);
  2515. fileTableBody.appendChild(row);
  2516. // 添加事件监听器
  2517. fileNameInput.addEventListener('input', function() {
  2518. updateFileConfig();
  2519. });
  2520. addressInput.addEventListener('input', function() {
  2521. updateFileConfig();
  2522. });
  2523. algorithmInput.addEventListener('input', function() {
  2524. updateFileConfig();
  2525. });
  2526. deleteBtn.addEventListener('click', function() {
  2527. deleteFileRow(row);
  2528. });
  2529. // 如果是新行,添加到配置中
  2530. if (!file && window.config && window.config.files) {
  2531. window.config.files.push({
  2532. fileName: '',
  2533. address: '',
  2534. algorithm: ''
  2535. });
  2536. }
  2537. }
  2538. // 删除文件行
  2539. function deleteFileRow(row) {
  2540. if (!fileTableBody || !row) return;
  2541. const index = parseInt(row.dataset.index);
  2542. if (window.config && window.config.files && index >= 0 && index < window.config.files.length) {
  2543. window.config.files.splice(index, 1);
  2544. }
  2545. fileTableBody.removeChild(row);
  2546. updateRowIndices();
  2547. if (window.updateCodePreview) {
  2548. window.updateCodePreview();
  2549. }
  2550. }
  2551. // 更新行索引
  2552. function updateRowIndices() {
  2553. if (!fileTableBody) return;
  2554. const rows = fileTableBody.querySelectorAll('.file-table-row');
  2555. rows.forEach((row, index) => {
  2556. row.dataset.index = index;
  2557. });
  2558. }
  2559. // 更新文件配置
  2560. function updateFileConfig() {
  2561. if (!fileTableBody || !window.config || !window.config.files) return;
  2562. const rows = fileTableBody.querySelectorAll('.file-table-row');
  2563. window.config.files = [];
  2564. rows.forEach(row => {
  2565. const inputs = row.querySelectorAll('input');
  2566. if (inputs.length >= 3) {
  2567. window.config.files.push({
  2568. fileName: inputs[0].value,
  2569. address: inputs[1].value,
  2570. algorithm: inputs[2].value
  2571. });
  2572. }
  2573. });
  2574. if (window.updateCodePreview) {
  2575. window.updateCodePreview();
  2576. }
  2577. }
  2578. // 清空所有文件
  2579. function clearAllFiles() {
  2580. if (!fileTableBody) return;
  2581. fileTableBody.innerHTML = '';
  2582. if (window.config && window.config.files) {
  2583. window.config.files.length = 0;
  2584. }
  2585. if (window.updateCodePreview) {
  2586. window.updateCodePreview();
  2587. }
  2588. }
  2589. // 三栏布局:sidebar切换逻辑
  2590. function setupSidebarPanelSwitch() {
  2591. const sidebarBtns = document.querySelectorAll('.sidebar-btn');
  2592. const panels = ['serialPanel', 'flmPanel', 'scriptPanel', 'varPanel'];
  2593. const monitorPanel = document.querySelector('.monitor-panel');
  2594. sidebarBtns.forEach(btn => {
  2595. btn.addEventListener('click', function() {
  2596. // 切换按钮active
  2597. sidebarBtns.forEach(b => b.classList.remove('active'));
  2598. this.classList.add('active');
  2599. // 切换内容区
  2600. panels.forEach(pid => {
  2601. const panel = document.getElementById(pid);
  2602. if (panel) panel.style.display = (this.dataset.panel === pid) ? 'block' : 'none';
  2603. });
  2604. // 控制右侧监控面板的显示/隐藏
  2605. if (monitorPanel) {
  2606. if (this.dataset.panel === 'varPanel') {
  2607. // 变量分析页面:隐藏监控面板
  2608. monitorPanel.style.display = 'none';
  2609. } else {
  2610. // 其他页面:显示监控面板
  2611. monitorPanel.style.display = '';
  2612. }
  2613. }
  2614. // 切换到Python脚本配置时初始化
  2615. if (this.dataset.panel === 'scriptPanel') {
  2616. setupPythonScriptPanel();
  2617. // 初始化多文件配置
  2618. initMultiFileConfig();
  2619. }
  2620. // 切换到变量分析时初始化
  2621. if (this.dataset.panel === 'varPanel') {
  2622. setupVarAnalysisPanel();
  2623. }
  2624. });
  2625. });
  2626. }
  2627. document.addEventListener('DOMContentLoaded', function() {
  2628. setupSidebarPanelSwitch();
  2629. });
  2630. // ... 现有代码 ...
  2631. function setupPythonScriptPanel() {
  2632. // 防止重复绑定
  2633. if (window._pythonPanelInited) return;
  2634. window._pythonPanelInited = true;
  2635. const swdClockSpeedMap = { '10M': '10000000', '5M': '5000000', '2M': '2000000', '1M': '1000000', '500K': '500000', '200K': '200000', '100K': '100000', '50K': '50000', '20K': '20000', '10K': '10000', '5K': '5000' };
  2636. const customFlmInput = document.getElementById('customFlm');
  2637. const address1Input = document.getElementById('address1');
  2638. const address2Input = document.getElementById('address2');
  2639. const binFileNameInput = document.getElementById('binFileName');
  2640. const swdClockSpeedSelect = document.getElementById('swdClockSpeed');
  2641. const codePreview = document.getElementById('codePreview');
  2642. const dragCodePreview = document.getElementById('dragCodePreview');
  2643. // 删除下载按钮相关代码
  2644. const pyYmodemSendBtn = document.getElementById('pyYmodemSendBtn');
  2645. const pyYmodemProgress = document.getElementById('pyYmodemProgress');
  2646. const pyYmodemLog = document.getElementById('pyYmodemLog');
  2647. // 使用全局config对象
  2648. if (typeof window.config === 'undefined') {
  2649. window.config = {
  2650. flmFile: (customFlmInput && customFlmInput.value) || 'custom_flm.FLM.o',
  2651. address1: (address1Input && address1Input.value) || '0X08000000',
  2652. address2: (address2Input && address2Input.value) || '0x20000000',
  2653. binFileName: (binFileNameInput && binFileNameInput.value) || 'ILI9341_HAL.bin',
  2654. swdClockSpeed: (swdClockSpeedSelect && swdClockSpeedSelect.value) || '10M',
  2655. files: [
  2656. {
  2657. fileName: 'boot.bin',
  2658. address: '0x08000000',
  2659. algorithm: 'STM32F7x_1024.FLM.o'
  2660. },
  2661. {
  2662. fileName: 'rtthread.bin',
  2663. address: '0x08020000',
  2664. algorithm: 'STM32F7x_1024.FLM.o'
  2665. },
  2666. {
  2667. fileName: 'HZK.bin',
  2668. address: '0x90000000',
  2669. algorithm: 'STM32F767_W25QXX.FLM.o'
  2670. }
  2671. ]
  2672. };
  2673. }
  2674. const config = window.config;
  2675. // 确保config对象有files数组
  2676. if (!config.files) {
  2677. config.files = [
  2678. {
  2679. fileName: 'boot.bin',
  2680. address: '0x08000000',
  2681. algorithm: 'STM32F7x_1024.FLM.o'
  2682. },
  2683. {
  2684. fileName: 'rtthread.bin',
  2685. address: '0x08020000',
  2686. algorithm: 'STM32F7x_1024.FLM.o'
  2687. },
  2688. {
  2689. fileName: 'HZK.bin',
  2690. address: '0x90000000',
  2691. algorithm: 'STM32F767_W25QXX.FLM.o'
  2692. }
  2693. ];
  2694. }
  2695. // 全局updateCodePreview函数
  2696. window.updateCodePreview = function() {
  2697. const flmFile = config.flmFile;
  2698. const pythonSwdSpeed = swdClockSpeedMap[config.swdClockSpeed] || '10000000';
  2699. // 生成多文件烧录代码
  2700. let offlineCode = `import FLMConfig\nimport PikaStdLib\nimport PikaStdDevice\nimport time\n\ntime = PikaStdDevice.Time()\nbuzzer = PikaStdDevice.GPIO()\nbuzzer.setPin('PA4')\nbuzzer.setMode('out')\n\n# 设置SWD下载速度\ncmd.set_swd_clock(${pythonSwdSpeed})\n\nReadFlm = FLMConfig.ReadFlm()`;
  2701. // 按算法分组文件
  2702. const algorithmGroups = {};
  2703. if (config.files && config.files.length > 0) {
  2704. config.files.forEach(file => {
  2705. if (file.algorithm && file.fileName && file.address) {
  2706. if (!algorithmGroups[file.algorithm]) {
  2707. algorithmGroups[file.algorithm] = [];
  2708. }
  2709. algorithmGroups[file.algorithm].push(file);
  2710. }
  2711. });
  2712. // 为每个算法生成加载和烧录代码
  2713. Object.keys(algorithmGroups).forEach((algorithm, index) => {
  2714. const files = algorithmGroups[algorithm];
  2715. if (files.length > 0) {
  2716. // 加载算法
  2717. offlineCode += `\n# 加载 ${algorithm} 下载算法文件\nresult = ReadFlm.load("FLM/${algorithm}", ${config.address1}, ${config.address2})\nif result != 0:\n return`;
  2718. // 烧录该算法下的所有文件
  2719. files.forEach(file => {
  2720. offlineCode += `\n\n# 烧写 ${file.fileName}\nresult = load.bin("${file.fileName}", ${file.address})\nif result != 0:\n return`;
  2721. });
  2722. }
  2723. });
  2724. } else {
  2725. // 如果没有多文件配置,使用默认的单文件配置
  2726. offlineCode += `\n# 加载 FLM 文件\nresult = ReadFlm.load("FLM/${flmFile}", ${config.address1}, ${config.address2})\nif result != 0:\n return \n\n# 烧写固件文件\nresult = load.bin("${config.binFileName || 'firmware.bin'}", ${config.address1})\nif result != 0:\n return`;
  2727. }
  2728. offlineCode += `\n\n# 蜂鸣器响一声,表示烧写完成\nbuzzer.enable()\nbuzzer.high()\ntime.sleep_ms(500)\nbuzzer.low()\ntime.sleep_ms(500)`;
  2729. const dragCode = `import FLMConfig\ncmd.set_swd_clock(${pythonSwdSpeed})\nReadFlm = FLMConfig.ReadFlm()\nres1 = ReadFlm.load("FLM/${flmFile}",${config.address1},${config.address2})`;
  2730. if (codePreview) {
  2731. codePreview.textContent = offlineCode;
  2732. }
  2733. if (dragCodePreview) {
  2734. dragCodePreview.textContent = dragCode;
  2735. }
  2736. // 高亮显示
  2737. let highlightedCode = offlineCode;
  2738. if (config.files && config.files.length > 0) {
  2739. config.files.forEach(file => {
  2740. if (file.fileName) {
  2741. highlightedCode = highlightedCode.replace(
  2742. new RegExp(`"${file.fileName}"`, 'g'),
  2743. `<span class="highlight">"${file.fileName}"</span>`
  2744. );
  2745. }
  2746. if (file.address) {
  2747. highlightedCode = highlightedCode.replace(
  2748. new RegExp(file.address.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'),
  2749. `<span class="highlight">${file.address}</span>`
  2750. );
  2751. }
  2752. if (file.algorithm) {
  2753. highlightedCode = highlightedCode.replace(
  2754. new RegExp(`"FLM/${file.algorithm}"`, 'g'),
  2755. `<span class="highlight">"FLM/${file.algorithm}"</span>`
  2756. );
  2757. }
  2758. });
  2759. }
  2760. // 高亮其他配置项
  2761. highlightedCode = highlightedCode.replace(`"FLM/${flmFile}"`, `<span class="highlight">"FLM/${flmFile}"</span>`)
  2762. .replace(new RegExp(config.address1.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${config.address1}</span>`)
  2763. .replace(new RegExp(config.address2.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${config.address2}</span>`)
  2764. .replace(new RegExp(pythonSwdSpeed.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${pythonSwdSpeed}</span>`);
  2765. if (codePreview) {
  2766. codePreview.innerHTML = highlightedCode;
  2767. }
  2768. if (dragCodePreview) {
  2769. dragCodePreview.innerHTML = dragCode.replace(`"FLM/${flmFile}"`, `<span class="highlight">"FLM/${flmFile}"</span>`)
  2770. .replace(new RegExp(config.address1.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${config.address1}</span>`)
  2771. .replace(new RegExp(config.address2.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${config.address2}</span>`)
  2772. .replace(new RegExp(pythonSwdSpeed.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${pythonSwdSpeed}</span>`);
  2773. }
  2774. }
  2775. if (customFlmInput) {
  2776. customFlmInput.addEventListener('input', function() { config.flmFile = this.value || 'custom_flm.FLM.o'; window.updateCodePreview(); });
  2777. }
  2778. if (address1Input) {
  2779. address1Input.addEventListener('input', function() { config.address1 = this.value || '0X08000000'; window.updateCodePreview(); });
  2780. }
  2781. if (address2Input) {
  2782. address2Input.addEventListener('input', function() { config.address2 = this.value || '0x20000000'; window.updateCodePreview(); });
  2783. }
  2784. if (binFileNameInput) {
  2785. binFileNameInput.addEventListener('input', function() { config.binFileName = this.value || 'firmware.bin'; window.updateCodePreview(); });
  2786. }
  2787. if (swdClockSpeedSelect) {
  2788. swdClockSpeedSelect.addEventListener('change', function() { config.swdClockSpeed = this.value; window.updateCodePreview(); });
  2789. }
  2790. // 删除下载按钮事件监听器
  2791. // YMODEM发送按钮状态和事件
  2792. function updatePyYmodemSendBtnState() {
  2793. const tab = document.querySelector('.script-tab.active');
  2794. if (tab && tab.textContent.includes('离线')) {
  2795. pyYmodemSendBtn.disabled = false;
  2796. pyYmodemSendBtn.setAttribute('data-pytype', 'offline');
  2797. } else if (tab && tab.textContent.includes('拖拽')) {
  2798. pyYmodemSendBtn.disabled = false;
  2799. pyYmodemSendBtn.setAttribute('data-pytype', 'drag');
  2800. } else {
  2801. pyYmodemSendBtn.disabled = true;
  2802. pyYmodemSendBtn.removeAttribute('data-pytype');
  2803. }
  2804. }
  2805. const scriptTabs = document.querySelectorAll('.script-tab');
  2806. if (scriptTabs.length > 0) {
  2807. scriptTabs.forEach(tab => {
  2808. tab.addEventListener('click', updatePyYmodemSendBtnState);
  2809. });
  2810. updatePyYmodemSendBtnState();
  2811. }
  2812. // Python日志输出到主终端
  2813. // Python日志输出到主终端
  2814. function pyYlog(msg, color) {
  2815. appendToTerminalOutput(`<div class='log-prefix-python'>[PYTHON] ${msg}</div>`);
  2816. }
  2817. function pyYlogClear() {
  2818. // 不清空主终端
  2819. }
  2820. // 统一的Python YMODEM发送函数
  2821. async function sendPythonYmodem(code, fileName) {
  2822. if (!window.microLinkTerminal || !window.microLinkTerminal.isConnected || !window.microLinkTerminal.port) {
  2823. pyYlog('请先连接串口', '#f66'); return;
  2824. }
  2825. pyYmodemSendOfflineBtn && (pyYmodemSendOfflineBtn.disabled = true);
  2826. pyYmodemSendDragBtn && (pyYmodemSendDragBtn.disabled = true);
  2827. pyYlogClear();
  2828. pyYlog('准备发送...', '#0ff');
  2829. let wasConnected = false;
  2830. if (window.microLinkTerminal) {
  2831. if (window.microLinkTerminal.reader) {
  2832. try { window.microLinkTerminal.reader.cancel(); } catch(e){}
  2833. try { window.microLinkTerminal.reader.releaseLock(); } catch(e){}
  2834. window.microLinkTerminal.reader = null;
  2835. }
  2836. wasConnected = window.microLinkTerminal.isConnected;
  2837. window.microLinkTerminal.isConnected = false;
  2838. await new Promise(r => setTimeout(r, 300));
  2839. }
  2840. try {
  2841. const uint8Array = new TextEncoder().encode(code);
  2842. const port = window.microLinkTerminal && window.microLinkTerminal.port;
  2843. if (!port) throw new Error('串口未连接');
  2844. let ok = await window.sendFileViaYmodem(
  2845. port,
  2846. uint8Array,
  2847. fileName,
  2848. uint8Array.length,
  2849. msg => pyYlog(msg)
  2850. );
  2851. if (ok) {
  2852. pyYlog('✅ 发送完成', '#0f0');
  2853. } else {
  2854. pyYlog('❌ 发送失败', '#f66');
  2855. }
  2856. } catch (e) {
  2857. pyYlog('❌ 发送失败: ' + e.message, '#f66');
  2858. if (e && e.stack) pyYlog('错误堆栈: ' + e.stack, '#f66');
  2859. } finally {
  2860. if (window.microLinkTerminal) {
  2861. window.microLinkTerminal.isConnected = wasConnected;
  2862. if (wasConnected && typeof window.microLinkTerminal.startReading === 'function') {
  2863. window.microLinkTerminal.startReading();
  2864. }
  2865. }
  2866. pyYmodemSendOfflineBtn && (pyYmodemSendOfflineBtn.disabled = false);
  2867. pyYmodemSendDragBtn && (pyYmodemSendDragBtn.disabled = false);
  2868. }
  2869. }
  2870. function getOfflineCode() {
  2871. const flmFile = config.flmFile;
  2872. const pythonSwdSpeed = swdClockSpeedMap[config.swdClockSpeed] || '10000000';
  2873. // 生成多文件烧录代码
  2874. let offlineCode = `import FLMConfig\nimport PikaStdLib\nimport PikaStdDevice\nimport time\n\ntime = PikaStdDevice.Time()\nbuzzer = PikaStdDevice.GPIO()\nbuzzer.setPin('PA4')\nbuzzer.setMode('out')\n\n# 设置SWD下载速度\ncmd.set_swd_clock(${pythonSwdSpeed})\n\nReadFlm = FLMConfig.ReadFlm()`;
  2875. // 按算法分组文件
  2876. const algorithmGroups = {};
  2877. config.files.forEach(file => {
  2878. if (file.algorithm && file.fileName && file.address) {
  2879. if (!algorithmGroups[file.algorithm]) {
  2880. algorithmGroups[file.algorithm] = [];
  2881. }
  2882. algorithmGroups[file.algorithm].push(file);
  2883. }
  2884. });
  2885. // 为每个算法生成加载和烧录代码
  2886. Object.keys(algorithmGroups).forEach((algorithm, index) => {
  2887. const files = algorithmGroups[algorithm];
  2888. if (files.length > 0) {
  2889. // 加载算法
  2890. offlineCode += `\n# 加载 ${algorithm} 下载算法文件\nresult = ReadFlm.load("FLM/${algorithm}", ${config.address1}, ${config.address2})\nif result != 0:\n return`;
  2891. // 烧录该算法下的所有文件
  2892. files.forEach(file => {
  2893. offlineCode += `\n\n# 烧写 ${file.fileName}\nresult = load.bin("${file.fileName}", ${file.address})\nif result != 0:\n return`;
  2894. });
  2895. }
  2896. });
  2897. offlineCode += `\n\n# 蜂鸣器响一声,表示烧写完成\nbuzzer.enable()\nbuzzer.high()\ntime.sleep_ms(500)\nbuzzer.low()\ntime.sleep_ms(500)`;
  2898. return offlineCode;
  2899. }
  2900. function getDragCode() {
  2901. const flmFile = config.flmFile;
  2902. const pythonSwdSpeed = swdClockSpeedMap[config.swdClockSpeed] || '10000000';
  2903. return `import FLMConfig\ncmd.set_swd_clock(${pythonSwdSpeed})\nReadFlm = FLMConfig.ReadFlm()\nres1 = ReadFlm.load(\"FLM/${flmFile}\",${config.address1},${config.address2})`;
  2904. }
  2905. // 删除旧的pyYmodemSendBtn事件绑定
  2906. // 统一事件绑定,确保使用正确的脚本内容
  2907. const pyYmodemSendOfflineBtn = document.getElementById('pyYmodemSendOfflineBtn');
  2908. const pyYmodemSendDragBtn = document.getElementById('pyYmodemSendDragBtn');
  2909. if (pyYmodemSendOfflineBtn) {
  2910. pyYmodemSendOfflineBtn.addEventListener('click', async () => {
  2911. await sendPythonYmodem(getOfflineCode(), 'Python/offline_download.py');
  2912. });
  2913. }
  2914. if (pyYmodemSendDragBtn) {
  2915. pyYmodemSendDragBtn.addEventListener('click', async () => {
  2916. await sendPythonYmodem(getDragCode(), 'Python/drag_download.py');
  2917. });
  2918. }
  2919. window.updateCodePreview();
  2920. }
  2921. // ... 现有代码 ...
  2922. // ... 只展示相关修改 ...
  2923. // 事件绑定移到函数定义之后
  2924. // 删除重复的全局getOfflineCode函数定义
  2925. function getDragCode() {
  2926. const customFlmInput = document.getElementById('customFlm');
  2927. const address1Input = document.getElementById('address1');
  2928. const address2Input = document.getElementById('address2');
  2929. const swdClockSpeedSelect = document.getElementById('swdClockSpeed');
  2930. const flmFile = customFlmInput ? customFlmInput.value || 'custom_flm.FLM.o' : 'custom_flm.FLM.o';
  2931. const address1 = address1Input ? address1Input.value || '0X08000000' : '0X08000000';
  2932. const address2 = address2Input ? address2Input.value || '0x20000000' : '0x20000000';
  2933. const swdClockSpeedMap = { '10M': '10000000', '5M': '5000000', '2M': '2000000', '1M': '1000000', '500K': '500000', '200K': '200000', '100K': '100000', '50K': '50000', '20K': '20000', '10K': '10000', '5K': '5000' };
  2934. const pythonSwdSpeed = swdClockSpeedMap[swdClockSpeedSelect ? swdClockSpeedSelect.value : '10M'] || '10000000';
  2935. return `import FLMConfig\ncmd.set_swd_clock(${pythonSwdSpeed})\nReadFlm = FLMConfig.ReadFlm()\nres1 = ReadFlm.load(\"FLM/${flmFile}\",${address1},${address2})`;
  2936. }
  2937. // 删除重复的sendPythonYmodem函数定义
  2938. // 删除重复的事件绑定,统一在setupPythonScriptPanel中处理
  2939. // FLM .o文件 YMODEM发送
  2940. async function handleFlmYmodemSend() {
  2941. const flmYmodemSendBtn = document.getElementById('flmYmodemSendBtn');
  2942. const flmYmodemProgress = document.getElementById('flmYmodemProgress');
  2943. const log = document.getElementById('log');
  2944. // 依赖 convertedBlob, flmFileName, isSerialConnected
  2945. if (!window.convertedBlob) {
  2946. if (log) log.textContent += '\n请先生成.o文件';
  2947. return;
  2948. }
  2949. if (!window.microLinkTerminal || !window.microLinkTerminal.isConnected || !window.microLinkTerminal.port) {
  2950. if (log) log.textContent += '\n请先连接串口';
  2951. return;
  2952. }
  2953. flmYmodemSendBtn.disabled = true;
  2954. flmYmodemProgress.style.display = '';
  2955. flmYmodemProgress.value = 0;
  2956. if (log) log.textContent += '\n准备发送...';
  2957. let wasConnected = false;
  2958. if (window.microLinkTerminal) {
  2959. if (window.microLinkTerminal.reader) {
  2960. try { window.microLinkTerminal.reader.cancel(); } catch(e){}
  2961. try { window.microLinkTerminal.reader.releaseLock(); } catch(e){}
  2962. window.microLinkTerminal.reader = null;
  2963. }
  2964. wasConnected = window.microLinkTerminal.isConnected;
  2965. window.microLinkTerminal.isConnected = false;
  2966. await new Promise(r => setTimeout(r, 300));
  2967. }
  2968. try {
  2969. const arrayBuffer = await window.convertedBlob.arrayBuffer();
  2970. const uint8Array = new Uint8Array(arrayBuffer);
  2971. // 修改点:加上 FLM/ 前缀
  2972. const fileName = 'FLM/' + (window.flmFileName ? (window.flmFileName + '.FLM.o') : 'firmware.FLM.o');
  2973. const port = window.microLinkTerminal && window.microLinkTerminal.port;
  2974. let ok = await window.sendFileViaYmodem(
  2975. port,
  2976. uint8Array,
  2977. fileName,
  2978. uint8Array.length,
  2979. progress => { flmYmodemProgress.value = progress; },
  2980. msg => { if (log) log.textContent += '\n' + msg; }
  2981. );
  2982. if (ok) {
  2983. if (log) log.textContent += '\n✅ 文件发送完成';
  2984. } else {
  2985. if (log) log.textContent += '\n❌ 发送失败';
  2986. }
  2987. } catch (e) {
  2988. if (log) log.textContent += '\n❌ 发送失败: ' + e.message;
  2989. } finally {
  2990. if (window.microLinkTerminal) {
  2991. window.microLinkTerminal.isConnected = wasConnected;
  2992. if (wasConnected && typeof window.microLinkTerminal.startReading === 'function') {
  2993. window.microLinkTerminal.startReading();
  2994. }
  2995. }
  2996. flmYmodemSendBtn.disabled = false;
  2997. flmYmodemProgress.style.display = 'none';
  2998. }
  2999. }
  3000. // Python脚本 YMODEM发送
  3001. async function handlePyYmodemSend(type) {
  3002. const pyYmodemSendOfflineBtn = document.getElementById('pyYmodemSendOfflineBtn');
  3003. const pyYmodemSendDragBtn = document.getElementById('pyYmodemSendDragBtn');
  3004. const pyYmodemProgress = document.getElementById('pyYmodemProgress');
  3005. const pyYmodemLog = document.getElementById('pyYmodemLog');
  3006. function pyYlog(msg, color) {
  3007. appendToTerminalOutput(`<div class='log-prefix-python'>[PYTHON] ${msg}</div>`);
  3008. }
  3009. function pyYlogClear() {
  3010. // 不清空主终端
  3011. }
  3012. if (!window.microLinkTerminal || !window.microLinkTerminal.isConnected || !window.microLinkTerminal.port) {
  3013. pyYlog('请先连接串口', '#f66'); return;
  3014. }
  3015. pyYmodemSendOfflineBtn && (pyYmodemSendOfflineBtn.disabled = true);
  3016. pyYmodemSendDragBtn && (pyYmodemSendDragBtn.disabled = true);
  3017. pyYmodemProgress && (pyYmodemProgress.value = 0);
  3018. pyYmodemProgress && (pyYmodemProgress.style.display = '');
  3019. pyYlogClear();
  3020. pyYlog('准备发送...', '#0ff');
  3021. let wasConnected = false;
  3022. if (window.microLinkTerminal) {
  3023. if (window.microLinkTerminal.reader) {
  3024. try { window.microLinkTerminal.reader.cancel(); } catch(e){}
  3025. try { window.microLinkTerminal.reader.releaseLock(); } catch(e){}
  3026. window.microLinkTerminal.reader = null;
  3027. }
  3028. wasConnected = window.microLinkTerminal.isConnected;
  3029. window.microLinkTerminal.isConnected = false;
  3030. await new Promise(r => setTimeout(r, 300));
  3031. }
  3032. try {
  3033. let code = '';
  3034. let fileName = '';
  3035. if (type === 'offline') {
  3036. code = window.getOfflineCode ? window.getOfflineCode() : '';
  3037. fileName = 'Python/offline_download.py';
  3038. } else if (type === 'drag') {
  3039. code = window.getDragCode ? window.getDragCode() : '';
  3040. fileName = 'Python/drag_download.py';
  3041. } else {
  3042. pyYlog('只允许发送离线下载脚本或拖拽下载脚本', '#f66');
  3043. return;
  3044. }
  3045. const uint8Array = new TextEncoder().encode(code);
  3046. const port = window.microLinkTerminal && window.microLinkTerminal.port;
  3047. let ok = await window.sendFileViaYmodem(
  3048. port,
  3049. uint8Array,
  3050. fileName,
  3051. uint8Array.length,
  3052. msg => pyYlog(msg)
  3053. );
  3054. if (ok) {
  3055. pyYlog('✅ 发送完成', '#0f0');
  3056. } else {
  3057. pyYlog('❌ 发送失败', '#f66');
  3058. }
  3059. } catch (e) {
  3060. pyYlog('❌ 发送失败: ' + e.message, '#f66');
  3061. if (e && e.stack) pyYlog('错误堆栈: ' + e.stack, '#f66');
  3062. } finally {
  3063. if (window.microLinkTerminal) {
  3064. window.microLinkTerminal.isConnected = wasConnected;
  3065. if (wasConnected && typeof window.microLinkTerminal.startReading === 'function') {
  3066. window.microLinkTerminal.startReading();
  3067. }
  3068. }
  3069. pyYmodemSendOfflineBtn && (pyYmodemSendOfflineBtn.disabled = false);
  3070. pyYmodemSendDragBtn && (pyYmodemSendDragBtn.disabled = false);
  3071. pyYmodemProgress && (pyYmodemProgress.style.display = 'none');
  3072. }
  3073. }
  3074. // ... existing code ...
  3075. function setupVarAnalysisPanel() {
  3076. // 只绑定一次
  3077. if (window._varPanelInited) return;
  3078. window._varPanelInited = true;
  3079. const fileInput = document.getElementById('axfFile');
  3080. const analyzeBtn = document.getElementById('analyzeBtn');
  3081. if (!fileInput || !analyzeBtn) return;
  3082. fileInput.addEventListener('change', function(e) {
  3083. if (this.files.length > 0) {
  3084. analyzeBtn.disabled = false;
  3085. const fileInfo = document.getElementById('fileInfo');
  3086. if (fileInfo) fileInfo.classList.add('d-none');
  3087. } else {
  3088. analyzeBtn.disabled = true;
  3089. }
  3090. });
  3091. }
  3092. // ... existing code ...
  3093. // ========== 变量分析tab实时曲线功能 ==========
  3094. let chartData = [];
  3095. let chartInstance = null;
  3096. let chartDrawEnabled = false;
  3097. // 将chartDrawEnabled挂载到全局,供数据处理函数使用
  3098. window.chartDrawEnabled = chartDrawEnabled;
  3099. function setupRealtimeChart() {
  3100. const chartDom = document.getElementById('realtimeChart');
  3101. if (!chartDom) return;
  3102. if (!window.echarts) return;
  3103. // 初始化图表数据
  3104. if (!window.chartData) {
  3105. window.chartData = [];
  3106. }
  3107. chartInstance = echarts.init(chartDom);
  3108. // 延迟初始化多变量图表管理器,确保DOM元素已创建
  3109. setTimeout(() => {
  3110. window.multiChartManager = new MultiChartManager();
  3111. console.log('多变量图表管理器初始化完成');
  3112. }, 200);
  3113. // 初始化后强制resize,确保图表尺寸正确
  3114. setTimeout(() => {
  3115. if (chartInstance) {
  3116. chartInstance.resize();
  3117. }
  3118. }, 100);
  3119. // 采用test.html的ECharts配置
  3120. chartInstance.setOption({
  3121. xAxis: {
  3122. type: 'value',
  3123. name: '时间 (秒)',
  3124. nameLocation: 'middle',
  3125. nameGap: 30
  3126. },
  3127. yAxis: {
  3128. type: 'value',
  3129. scale: true,
  3130. name: '数值',
  3131. nameLocation: 'middle',
  3132. nameGap: 40
  3133. },
  3134. series: [{
  3135. type: 'line',
  3136. data: [],
  3137. smooth: true,
  3138. symbol: 'none',
  3139. lineStyle: { width: 2, color: '#3498db' },
  3140. name: '实时数据'
  3141. }],
  3142. grid: {
  3143. left: 80,
  3144. right: 40,
  3145. top: 50,
  3146. bottom: 80,
  3147. containLabel: true
  3148. },
  3149. animation: false,
  3150. tooltip: {
  3151. trigger: 'axis',
  3152. formatter: function(params) {
  3153. const data = params[0];
  3154. return `时间: ${data.value[0].toFixed(2)}s<br/>数值: ${data.value[1]}`;
  3155. }
  3156. }
  3157. });
  3158. document.getElementById('clearChartBtn').onclick = function() {
  3159. window.chartData = [];
  3160. chartTimeData = []; // 清除时间数据
  3161. chartDataBuffer = []; // 清除缓冲区
  3162. dataIntegrityErrors = 0; // 重置错误计数
  3163. expectedDataPattern = null; // 重置数据模式
  3164. lastProcessedTime = 0; // 重置时间戳
  3165. // 停止Worker
  3166. if (chartDataWorker) {
  3167. chartDataWorker.postMessage({ type: 'stop' });
  3168. }
  3169. if (chartRenderWorker) {
  3170. chartRenderWorker.postMessage({ type: 'stop' });
  3171. }
  3172. // 清空多变量图表
  3173. if (window.multiChartManager) {
  3174. window.multiChartManager.clearAllCharts();
  3175. }
  3176. // 清空图表数据,不调用updateRealtimeChart避免覆盖时间轴逻辑
  3177. if (chartInstance) {
  3178. chartInstance.setOption({
  3179. series: [{
  3180. data: []
  3181. }]
  3182. });
  3183. }
  3184. // 重置开始绘制按钮状态
  3185. const startBtn = document.getElementById('startBtn');
  3186. if (startBtn) {
  3187. startBtn.textContent = '开始绘制';
  3188. startBtn.className = 'btn btn-success';
  3189. }
  3190. chartDrawEnabled = false;
  3191. window.chartDrawEnabled = chartDrawEnabled;
  3192. console.log('[曲线模式] 已清除数据并停止绘制');
  3193. console.log('[曲线缓冲] 已清除缓冲区');
  3194. console.log('[数据完整性] 已重置错误计数器和数据模式');
  3195. console.log('[Worker] 已停止数据处理Worker');
  3196. console.log('[数据帧解析] 已清空数据帧缓冲区');
  3197. };
  3198. // 开始绘制按钮
  3199. const startBtn = document.getElementById('startChartBtn');
  3200. if (startBtn) {
  3201. startBtn.onclick = function() {
  3202. if (!chartDrawEnabled) {
  3203. // 开始绘制
  3204. chartDrawEnabled = true;
  3205. window.chartDrawEnabled = chartDrawEnabled;
  3206. startBtn.textContent = '暂停绘制';
  3207. startBtn.className = 'btn btn-warning';
  3208. // 自动开启HEX模式
  3209. const hexModeCheckbox = document.getElementById('hexMode');
  3210. if (hexModeCheckbox && !hexModeCheckbox.checked) {
  3211. hexModeCheckbox.checked = true;
  3212. hexModeCheckbox.dispatchEvent(new Event('change'));
  3213. }
  3214. // 在控制台显示曲线模式已启动
  3215. console.log('[曲线模式] 已启动 - 串口数据将直接用于曲线绘制,不显示在主监控');
  3216. console.log('[曲线模式] 请确保设备发送4字节对齐的HEX数据');
  3217. // 清除缓冲区,准备接收新数据
  3218. chartDataBuffer = [];
  3219. chartTimeData = []; // 清除时间数据
  3220. dataIntegrityErrors = 0; // 重置错误计数
  3221. expectedDataPattern = null; // 重置数据模式
  3222. lastProcessedTime = 0; // 重置时间戳
  3223. // 初始化Worker
  3224. initChartWorkers();
  3225. // 启动Worker
  3226. if (chartDataWorker) {
  3227. chartDataWorker.postMessage({ type: 'start' });
  3228. }
  3229. if (chartRenderWorker) {
  3230. chartRenderWorker.postMessage({ type: 'start' });
  3231. }
  3232. console.log('[曲线缓冲] 已清除缓冲区,准备接收新数据');
  3233. console.log('[数据完整性] 已重置错误计数器和数据模式');
  3234. console.log('[Worker] 已启动数据处理Worker');
  3235. } else {
  3236. // 暂停绘制
  3237. chartDrawEnabled = false;
  3238. window.chartDrawEnabled = chartDrawEnabled;
  3239. startBtn.textContent = '继续绘制';
  3240. startBtn.className = 'btn btn-success';
  3241. // 停止Worker
  3242. if (chartDataWorker) {
  3243. chartDataWorker.postMessage({ type: 'stop' });
  3244. }
  3245. if (chartRenderWorker) {
  3246. chartRenderWorker.postMessage({ type: 'stop' });
  3247. }
  3248. console.log('[曲线模式] 已暂停 - 恢复正常监控显示');
  3249. console.log('[Worker] 已停止数据处理Worker');
  3250. }
  3251. };
  3252. }
  3253. // 终止绘制按钮逻辑
  3254. const stopChartBtn = document.getElementById('stopChartBtn');
  3255. if (stopChartBtn) {
  3256. stopChartBtn.onclick = function() {
  3257. console.log('[终止绘制] 按钮被点击');
  3258. // 1. 清空曲线数据
  3259. window.chartData = [];
  3260. chartTimeData = []; // 清除时间数据
  3261. chartDataBuffer = []; // 清除缓冲区
  3262. dataIntegrityErrors = 0; // 重置错误计数
  3263. expectedDataPattern = null; // 重置数据模式
  3264. lastProcessedTime = 0; // 重置时间戳
  3265. // 2. 停止曲线绘制
  3266. if (window.chartDrawEnabled) {
  3267. window.chartDrawEnabled = false;
  3268. chartDrawEnabled = false;
  3269. // 更新按钮状态
  3270. const startBtn = document.getElementById('startChartBtn');
  3271. if (startBtn) {
  3272. startBtn.textContent = '开始绘制';
  3273. startBtn.className = 'btn btn-success';
  3274. }
  3275. // 停止Worker
  3276. if (window.chartDataWorker) {
  3277. window.chartDataWorker.postMessage({ type: 'stop' });
  3278. }
  3279. if (window.chartRenderWorker) {
  3280. window.chartRenderWorker.postMessage({ type: 'stop' });
  3281. }
  3282. console.log('[终止绘制] 曲线绘制已停止');
  3283. }
  3284. // 3. 清空多变量图表
  3285. if (window.multiChartManager) {
  3286. window.multiChartManager.clearAllCharts();
  3287. }
  3288. // 4. 清空图表显示
  3289. if (chartInstance) {
  3290. chartInstance.setOption({
  3291. series: [{
  3292. data: []
  3293. }]
  3294. });
  3295. }
  3296. // 5. 发送终止绘制命令(如果串口已连接)
  3297. const stopCommand = 'cmd.read_ram(0x20000000,1,0)';
  3298. if (window.microLinkTerminal && window.microLinkTerminal.isConnected) {
  3299. window.microLinkTerminal.sendCommand(stopCommand);
  3300. console.log('[终止绘制] 已发送停止命令:', stopCommand);
  3301. } else {
  3302. console.log('[终止绘制] 串口未连接,跳过发送命令');
  3303. }
  3304. // 6. 关闭监听框的HEX模式
  3305. const hexModeCheckbox = document.getElementById('hexMode');
  3306. if (hexModeCheckbox && hexModeCheckbox.checked) {
  3307. hexModeCheckbox.checked = false;
  3308. hexModeCheckbox.dispatchEvent(new Event('change'));
  3309. console.log('[终止绘制] 已关闭HEX模式');
  3310. }
  3311. console.log('[终止绘制] 操作完成 - 已清空曲线并停止绘制');
  3312. };
  3313. }
  3314. // 保证全局可用(每次都强制挂载,防止tab切换后失效)
  3315. window.chartData = window.chartData || [];
  3316. window.updateRealtimeChart = updateRealtimeChart;
  3317. // 旧的handleRealtimeHexData已删除,现在使用Worker方式
  3318. window.chartInstance = chartInstance;
  3319. // 再次初始化终端区监听,防止tab切换后丢失
  3320. if (!window._terminalHexChartSyncInited) {
  3321. setupTerminalHexChartSync();
  3322. window._terminalHexChartSyncInited = true;
  3323. }
  3324. }
  3325. // 全局时间轴数据
  3326. let chartTimeData = [];
  3327. // 将chartTimeData挂载到全局,供多变量图表使用
  3328. window.chartTimeData = chartTimeData;
  3329. function updateRealtimeChart() {
  3330. if (!chartInstance) return;
  3331. // 确保有足够的数据点来显示X轴
  3332. const data = window.chartData || [];
  3333. const timeData = chartTimeData || [];
  3334. // 如果时间数据不足,补充时间数据
  3335. while (timeData.length < data.length) {
  3336. const currentTime = new Date();
  3337. timeData.push(currentTime.toLocaleTimeString('zh-CN', {
  3338. hour12: false,
  3339. hour: '2-digit',
  3340. minute: '2-digit',
  3341. second: '2-digit',
  3342. fractionalSecondDigits: 3
  3343. }));
  3344. }
  3345. // 动态计算标签间隔,避免重叠
  3346. const maxLabels = 15; // 最多显示15个时间标签
  3347. const interval = Math.max(0, Math.floor(timeData.length / maxLabels));
  3348. chartInstance.setOption({
  3349. xAxis: {
  3350. type: 'category',
  3351. data: timeData,
  3352. axisLabel: {
  3353. show: true,
  3354. interval: interval,
  3355. rotate: 45, // 旋转标签避免重叠
  3356. fontSize: 10,
  3357. margin: 12,
  3358. formatter: function(value) {
  3359. // 只显示时:分:秒,不显示毫秒
  3360. return value.split('.')[0];
  3361. }
  3362. },
  3363. axisTick: {
  3364. show: true,
  3365. interval: interval
  3366. },
  3367. name: '时间',
  3368. nameLocation: 'middle',
  3369. nameGap: 30
  3370. },
  3371. yAxis: {
  3372. type: 'value',
  3373. scale: true,
  3374. axisLabel: {
  3375. show: true,
  3376. fontSize: 10
  3377. },
  3378. name: '数值',
  3379. nameLocation: 'middle',
  3380. nameGap: 40
  3381. },
  3382. series: [{
  3383. type: 'line',
  3384. data: data,
  3385. smooth: true,
  3386. symbol: 'none',
  3387. lineStyle: { width: 2, color: '#3498db' },
  3388. name: '实时数据'
  3389. }],
  3390. tooltip: {
  3391. trigger: 'axis',
  3392. formatter: function(params) {
  3393. const data = params[0];
  3394. const timeStr = timeData[data.dataIndex] || '未知时间';
  3395. return `时间: ${timeStr}<br/>数值: ${data.value}`;
  3396. }
  3397. }
  3398. });
  3399. // 强制重新计算布局
  3400. setTimeout(() => {
  3401. if (chartInstance) {
  3402. chartInstance.resize();
  3403. // 非全屏时也强制重新计算,模拟全屏的效果
  3404. setTimeout(() => {
  3405. chartInstance.resize();
  3406. }, 50);
  3407. }
  3408. }, 100);
  3409. }
  3410. // 全局缓冲区用于处理分片数据
  3411. let chartDataBuffer = [];
  3412. let lastProcessedTime = 0;
  3413. let dataIntegrityErrors = 0;
  3414. let expectedDataPattern = null; // 用于检测数据模式
  3415. let chartDataWorker = null; // 数据处理Worker
  3416. let chartRenderWorker = null; // 渲染Worker
  3417. // 初始化Worker
  3418. function initChartWorkers() {
  3419. if (chartDataWorker || chartRenderWorker) return;
  3420. // 数据Worker脚本 - 采用test.html的简单策略
  3421. const dataWorkerScript = `
  3422. let dataBuffer = [];
  3423. let isRunning = false;
  3424. let lastReportTime = 0;
  3425. onmessage = function(e) {
  3426. switch (e.data.type) {
  3427. case 'start':
  3428. isRunning = true;
  3429. dataBuffer = [];
  3430. lastReportTime = performance.now();
  3431. break;
  3432. case 'stop':
  3433. isRunning = false;
  3434. break;
  3435. case 'serial_data':
  3436. if (isRunning) {
  3437. // 使用传入的时间戳,完全采用test.html的方式
  3438. e.data.values.forEach(val => {
  3439. dataBuffer.push({ timestamp: e.data.timestamp, value: val });
  3440. });
  3441. // 每100个数据点或100ms发送一次(参考test.html)
  3442. if (dataBuffer.length >= 100 || performance.now() - lastReportTime > 100) {
  3443. postMessage({
  3444. type: 'data_batch',
  3445. data: dataBuffer.splice(0, dataBuffer.length)
  3446. });
  3447. lastReportTime = performance.now();
  3448. }
  3449. }
  3450. break;
  3451. }
  3452. };
  3453. `;
  3454. // 渲染Worker脚本 - 完全采用test.html的逻辑
  3455. const renderWorkerScript = `
  3456. let isRunning = false;
  3457. let renderRate = 60;
  3458. let maxDataPoints = 10000;
  3459. let dataBuffer = [];
  3460. let lastRenderTime = 0;
  3461. let renderCount = 0;
  3462. let startTime = 0;
  3463. let totalDataPoints = 0;
  3464. let timeWindow = 5;
  3465. let historyPosition = 1.0;
  3466. function processData() {
  3467. if (!isRunning) return;
  3468. const now = performance.now();
  3469. if (now - lastRenderTime >= 1000 / renderRate) {
  3470. const chartData = prepareChartData();
  3471. postMessage({
  3472. type: 'render',
  3473. data: chartData,
  3474. stats: {
  3475. renderRate: renderCount / ((now - startTime) / 1000),
  3476. totalDataPoints: totalDataPoints,
  3477. dataBufferSize: dataBuffer.length
  3478. }
  3479. });
  3480. lastRenderTime = now;
  3481. renderCount++;
  3482. }
  3483. if (isRunning) {
  3484. setTimeout(processData, 1000 / renderRate);
  3485. }
  3486. }
  3487. function prepareChartData() {
  3488. if (dataBuffer.length === 0) return [];
  3489. // 根据历史位置计算要显示的数据范围
  3490. const totalPoints = dataBuffer.length;
  3491. const startIndex = Math.floor(totalPoints * (1 - historyPosition));
  3492. const endIndex = totalPoints;
  3493. // 获取指定范围的数据
  3494. const filteredData = dataBuffer.slice(startIndex, endIndex);
  3495. // 返回数据,时间轴显示实际时间(秒)- 完全采用test.html的方式
  3496. const result = filteredData.map(item => ({
  3497. time: (item.timestamp - dataBuffer[0].timestamp) / 1000,
  3498. value: item.value
  3499. }));
  3500. // 移除调试信息,时间轴已经正常工作
  3501. return result;
  3502. }
  3503. function compressData() {
  3504. // 限制数据点数量,避免内存占用过大
  3505. if (dataBuffer.length > maxDataPoints) {
  3506. const step = Math.ceil(dataBuffer.length / maxDataPoints);
  3507. dataBuffer = dataBuffer.filter((_, index) => index % step === 0);
  3508. }
  3509. }
  3510. function cleanupOldData() {
  3511. const now = performance.now();
  3512. // 保留最近60秒的数据,避免内存无限增长
  3513. const cutoffTime = now - 60000;
  3514. dataBuffer = dataBuffer.filter(item => item.timestamp >= cutoffTime);
  3515. }
  3516. onmessage = function(e) {
  3517. switch (e.data.type) {
  3518. case 'start':
  3519. isRunning = true;
  3520. renderRate = e.data.renderRate || 60;
  3521. maxDataPoints = e.data.maxDataPoints || 10000;
  3522. timeWindow = e.data.timeWindow || 5;
  3523. historyPosition = e.data.historyPosition || 1.0;
  3524. startTime = performance.now();
  3525. renderCount = 0;
  3526. totalDataPoints = 0;
  3527. dataBuffer = [];
  3528. lastRenderTime = 0;
  3529. processData();
  3530. break;
  3531. case 'stop':
  3532. isRunning = false;
  3533. break;
  3534. case 'data_batch':
  3535. dataBuffer.push(...e.data.data);
  3536. totalDataPoints += e.data.data.length;
  3537. compressData();
  3538. cleanupOldData();
  3539. break;
  3540. case 'config':
  3541. historyPosition = e.data.historyPosition || historyPosition;
  3542. break;
  3543. case 'clear':
  3544. dataBuffer = [];
  3545. totalDataPoints = 0;
  3546. break;
  3547. case 'get_stats':
  3548. postMessage({
  3549. type: 'stats',
  3550. stats: {
  3551. renderRate: renderCount / ((performance.now() - startTime) / 1000),
  3552. totalDataPoints: totalDataPoints,
  3553. dataBufferSize: dataBuffer.length
  3554. }
  3555. });
  3556. break;
  3557. }
  3558. };
  3559. `;
  3560. const dataWorkerBlob = new Blob([dataWorkerScript], { type: 'application/javascript' });
  3561. const renderWorkerBlob = new Blob([renderWorkerScript], { type: 'application/javascript' });
  3562. chartDataWorker = new Worker(URL.createObjectURL(dataWorkerBlob));
  3563. chartRenderWorker = new Worker(URL.createObjectURL(renderWorkerBlob));
  3564. // 设置Worker消息处理
  3565. chartDataWorker.onmessage = (e) => {
  3566. if (e.data.type === 'data_batch') {
  3567. chartRenderWorker.postMessage({
  3568. type: 'data_batch',
  3569. data: e.data.data
  3570. });
  3571. }
  3572. };
  3573. chartRenderWorker.onmessage = (e) => {
  3574. if (e.data.type === 'render') {
  3575. updateChartFromWorker(e.data.data);
  3576. }
  3577. };
  3578. console.log('[Worker] 图表Worker已初始化');
  3579. }
  3580. // 停止图表绘制
  3581. function stopChartDrawing() {
  3582. chartDrawEnabled = false;
  3583. window.chartDrawEnabled = chartDrawEnabled;
  3584. // 停止Worker
  3585. if (chartDataWorker) {
  3586. chartDataWorker.postMessage({ type: 'stop' });
  3587. }
  3588. if (chartRenderWorker) {
  3589. chartRenderWorker.postMessage({ type: 'stop' });
  3590. }
  3591. // 重置性能统计
  3592. performanceStats = {
  3593. dataPointsReceived: 0,
  3594. renderCount: 0,
  3595. lastRenderTime: 0,
  3596. averageRenderTime: 0
  3597. };
  3598. // 重置数据质量统计
  3599. dataQualityStats = {
  3600. totalBytesReceived: 0,
  3601. validDataPoints: 0,
  3602. errorCount: 0,
  3603. recoveryCount: 0,
  3604. lastReportTime: 0
  3605. };
  3606. // 更新按钮状态
  3607. const startBtn = document.getElementById('startChartBtn');
  3608. if (startBtn) {
  3609. startBtn.textContent = '开始绘制';
  3610. startBtn.className = 'btn btn-success';
  3611. }
  3612. console.log('[性能保护] 已自动停止图表绘制');
  3613. }
  3614. // 性能监控变量
  3615. let performanceStats = {
  3616. dataPointsReceived: 0,
  3617. renderCount: 0,
  3618. lastRenderTime: 0,
  3619. averageRenderTime: 0
  3620. };
  3621. // 数据质量统计
  3622. let dataQualityStats = {
  3623. totalBytesReceived: 0,
  3624. validDataPoints: 0,
  3625. errorCount: 0,
  3626. recoveryCount: 0,
  3627. lastReportTime: 0
  3628. };
  3629. // 从Worker更新图表
  3630. // 采用test.html的updateChart逻辑
  3631. function updateChartFromWorker(data) {
  3632. if (!chartInstance || !chartDrawEnabled) return;
  3633. const startTime = performance.now();
  3634. // 采用test.html的数据格式:直接使用time和value
  3635. const chartData = data.map(d => [d.time, d.value]);
  3636. // 更新性能统计
  3637. performanceStats.dataPointsReceived += data.length;
  3638. performanceStats.renderCount++;
  3639. const renderTime = performance.now() - startTime;
  3640. performanceStats.averageRenderTime = (performanceStats.averageRenderTime * (performanceStats.renderCount - 1) + renderTime) / performanceStats.renderCount;
  3641. // 每10次渲染输出一次性能统计
  3642. //if (performanceStats.renderCount % 10 === 0) {
  3643. //console.log(`[性能统计] 数据点: ${performanceStats.dataPointsReceived}, 渲染次数: ${performanceStats.renderCount}, 平均渲染时间: ${performanceStats.averageRenderTime.toFixed(2)}ms`);
  3644. //}
  3645. // 性能保护:如果渲染时间过长,自动停止
  3646. if (renderTime > 100) {
  3647. console.warn(`[性能警告] 渲染时间过长: ${renderTime.toFixed(2)}ms,自动停止绘制`);
  3648. stopChartDrawing();
  3649. return;
  3650. }
  3651. // 自适应性能调节
  3652. if (renderTime > 50 && performanceStats.renderCount > 20) {
  3653. console.warn(`[性能调节] 渲染时间较长: ${renderTime.toFixed(2)}ms,建议降低采样率或关闭完整性检查`);
  3654. }
  3655. // 完全采用test.html的updateChart逻辑
  3656. const chartDataArray = data.map(d => [d.time, d.value]);
  3657. const option = {
  3658. series: [{
  3659. data: chartDataArray
  3660. }]
  3661. };
  3662. // 采用新工程的x轴配置方式,不设置min/max,让ECharts自动计算合适的范围
  3663. if (data.length > 0) {
  3664. option.xAxis = {
  3665. type: 'value',
  3666. name: '时间 (秒)',
  3667. nameLocation: 'middle',
  3668. nameGap: 30
  3669. };
  3670. }
  3671. chartInstance.setOption(option);
  3672. }
  3673. function handleRealtimeHexDataChunked(data) {
  3674. if (!chartDrawEnabled) return;
  3675. if (!(data instanceof Uint8Array)) return;
  3676. const currentTime = Date.now();
  3677. // 更新数据质量统计
  3678. dataQualityStats.totalBytesReceived += data.length;
  3679. // 采用test.html的数组方式处理数据
  3680. // 将Uint8Array转换为数组,便于使用push和splice
  3681. if (!Array.isArray(chartDataBuffer)) {
  3682. chartDataBuffer = Array.from(chartDataBuffer);
  3683. }
  3684. // 采用test.html的数据转换方式:逐个字节转换
  3685. const values = [];
  3686. for (let i = 0; i < data.length; i++) {
  3687. values.push(data[i]);
  3688. }
  3689. // 将新数据添加到缓冲区
  3690. chartDataBuffer.push(...values);
  3691. // 调试输出:显示拼接后的数据
  3692. if (chartDataBuffer.length > 0) {
  3693. const firstBytes = chartDataBuffer.slice(0, Math.min(8, chartDataBuffer.length))
  3694. .map(b => b.toString(16).padStart(2, '0')).join(' ');
  3695. console.log(`[数据拼接] 缓冲区大小: ${chartDataBuffer.length}, 前8字节: ${firstBytes}`);
  3696. // 简化调试输出,只在调试模式下显示详细信息
  3697. const debugCheckbox = document.getElementById('debugMode');
  3698. const enableDebug = debugCheckbox && debugCheckbox.checked;
  3699. if (enableDebug) {
  3700. // 如果剩余字节不是4的倍数,记录警告
  3701. if (chartDataBuffer.length % 4 !== 0) {
  3702. console.warn(`[边界警告] 缓冲区大小 ${chartDataBuffer.length} 不是4的倍数`);
  3703. }
  3704. }
  3705. }
  3706. console.log(`[曲线缓冲] 缓冲区大小: ${chartDataBuffer.length} 字节`);
  3707. // 采用test.html的即时处理策略:每次接收到数据就立即处理
  3708. // 处理完整的4字节组
  3709. while (chartDataBuffer.length >= 4) {
  3710. // 采用test.html的简单策略:不做任何数据验证,直接处理所有数据
  3711. const value = (chartDataBuffer[0] | (chartDataBuffer[1]<<8) | (chartDataBuffer[2]<<16) | (chartDataBuffer[3]<<24)) >>> 0;
  3712. const seg = [chartDataBuffer[0], chartDataBuffer[1], chartDataBuffer[2], chartDataBuffer[3]];
  3713. // 发送数据到Worker
  3714. if (chartDataWorker) {
  3715. chartDataWorker.postMessage({
  3716. type: 'serial_data',
  3717. values: [value]
  3718. });
  3719. }
  3720. // 在控制台打印详细信息
  3721. console.log(`[曲线解析] ✅ 4字节: ${seg.map(x=>x.toString(16).padStart(2,'0')).join(' ')} -> 小端解析: 0x${value.toString(16).padStart(8,'0')} (${value})`);
  3722. // 更新最后处理时间
  3723. lastProcessedTime = currentTime;
  3724. // 重置错误计数
  3725. dataIntegrityErrors = 0;
  3726. // 更新统计
  3727. dataQualityStats.validDataPoints++;
  3728. // 移除已处理的4字节(参考test.html的splice方法)
  3729. chartDataBuffer.splice(0, 4);
  3730. }
  3731. // 移除已处理的4字节(参考test.html的splice方法)
  3732. chartDataBuffer.splice(0, 4);
  3733. }
  3734. // 检查缓冲区是否积压过多(可能数据丢失)
  3735. if (chartDataBuffer.length > 20) {
  3736. console.warn(`[曲线警告] 缓冲区积压过多 (${chartDataBuffer.length} 字节),可能数据丢失`);
  3737. chartDataBuffer = [];
  3738. dataIntegrityErrors++;
  3739. }
  3740. // 定期输出数据质量统计
  3741. const now = performance.now();
  3742. if (now - dataQualityStats.lastReportTime > 5000) { // 每5秒输出一次
  3743. const errorRate = dataQualityStats.totalBytesReceived > 0 ?
  3744. (dataQualityStats.errorCount / dataQualityStats.totalBytesReceived * 100).toFixed(2) : '0.00';
  3745. const recoveryRate = dataQualityStats.errorCount > 0 ?
  3746. (dataQualityStats.recoveryCount / dataQualityStats.errorCount * 100).toFixed(2) : '0.00';
  3747. console.log(`[数据质量统计] 总字节: ${dataQualityStats.totalBytesReceived}, 有效数据点: ${dataQualityStats.validDataPoints}, 错误: ${dataQualityStats.errorCount}, 恢复: ${dataQualityStats.recoveryCount}, 错误率: ${errorRate}%, 恢复率: ${recoveryRate}%`);
  3748. dataQualityStats.lastReportTime = now;
  3749. }
  3750. console.log(`[曲线处理] 完成处理,剩余缓冲区: ${chartDataBuffer.length} 字节,错误次数: ${dataIntegrityErrors}`);
  3751. // 智能数据恢复函数
  3752. function attemptSmartRecovery() {
  3753. if (chartDataBuffer.length < 8) {
  3754. return false; // 缓冲区数据不足
  3755. }
  3756. console.log(`[数据恢复] 尝试智能恢复,缓冲区大小: ${chartDataBuffer.length}`);
  3757. // 尝试不同的偏移量
  3758. for (let offset = 1; offset <= 4 && chartDataBuffer.length >= 4 + offset; offset++) {
  3759. const testBytes = [
  3760. chartDataBuffer[offset],
  3761. chartDataBuffer[offset+1],
  3762. chartDataBuffer[offset+2],
  3763. chartDataBuffer[offset+3]
  3764. ];
  3765. const testValue = (testBytes[0] | (testBytes[1]<<8) | (testBytes[2]<<16) | (testBytes[3]<<24)) >>> 0;
  3766. // 检查这个偏移量是否产生合理的数据
  3767. if (testValue > 0 && testValue <= 1000000) {
  3768. console.log(`[数据恢复] 找到有效偏移量: ${offset},新值: ${testValue}`);
  3769. chartDataBuffer = chartDataBuffer.slice(offset);
  3770. dataQualityStats.recoveryCount++;
  3771. return true;
  3772. }
  3773. }
  3774. // 如果找不到有效偏移量,丢弃前4字节
  3775. console.log(`[数据恢复] 未找到有效偏移量,丢弃前4字节`);
  3776. chartDataBuffer = chartDataBuffer.slice(4);
  3777. return false;
  3778. }
  3779. // 数据完整性检查函数
  3780. function checkDataIntegrity(bytes, value) {
  3781. // 检查1: 数值合理性(不能为0或过大)
  3782. if (value === 0) {
  3783. console.warn(`[完整性检查] 数值为0,可能数据丢失`);
  3784. return false;
  3785. }
  3786. if (value > 0x7FFFFFFF) {
  3787. console.warn(`[完整性检查] 数值过大 (${value}),可能数据错位`);
  3788. return false;
  3789. }
  3790. // 检查2: 时间间隔合理性(放宽限制)
  3791. const currentTime = Date.now();
  3792. if (lastProcessedTime > 0 && (currentTime - lastProcessedTime) > 30000) { // 改为30秒
  3793. console.warn(`[完整性检查] 数据间隔过长 (${currentTime - lastProcessedTime}ms),可能数据丢失`);
  3794. return false;
  3795. }
  3796. // 检查3: 数据模式一致性(只在有足够数据且模式稳定时检查)
  3797. if (expectedDataPattern !== null && window.chartData && window.chartData.length > 10) {
  3798. const patternMatch = checkDataPattern(bytes);
  3799. if (!patternMatch) {
  3800. console.warn(`[完整性检查] 数据模式不匹配,可能数据错位`);
  3801. return false;
  3802. }
  3803. }
  3804. // 建立数据模式(前几个数据点)
  3805. if (window.chartData && window.chartData.length < 3) {
  3806. establishDataPattern(bytes);
  3807. }
  3808. return true;
  3809. }
  3810. // 建立数据模式
  3811. function establishDataPattern(bytes) {
  3812. if (expectedDataPattern === null) {
  3813. expectedDataPattern = {
  3814. firstByte: bytes[0],
  3815. secondByte: bytes[1],
  3816. pattern: []
  3817. };
  3818. console.log(`[模式建立] 建立数据模式: 首字节=${bytes[0].toString(16).padStart(2,'0')}, 次字节=${bytes[1].toString(16).padStart(2,'0')}`);
  3819. }
  3820. }
  3821. // 检查数据模式
  3822. function checkDataPattern(bytes) {
  3823. if (expectedDataPattern === null) return true;
  3824. // 放宽模式检查:只要前两个字节不是完全相同的固定值就认为有效
  3825. // 这样可以适应数据变化的情况
  3826. if (bytes[0] !== bytes[1]) {
  3827. return true;
  3828. }
  3829. // 如果前两个字节相同,可能是固定模式,需要进一步检查
  3830. if (bytes[0] === expectedDataPattern.firstByte && bytes[1] === expectedDataPattern.secondByte) {
  3831. return true;
  3832. }
  3833. return false;
  3834. }
  3835. // 尝试数据恢复
  3836. function attemptDataRecovery() {
  3837. console.log(`[数据恢复] 尝试在缓冲区中寻找有效数据边界...`);
  3838. // 寻找可能的4字节边界
  3839. for (let i = 1; i < chartDataBuffer.length - 3; i++) {
  3840. const testBytes = [chartDataBuffer[i], chartDataBuffer[i+1], chartDataBuffer[i+2], chartDataBuffer[i+3]];
  3841. const testValue = (testBytes[0] | (testBytes[1]<<8) | (testBytes[2]<<16) | (testBytes[3]<<24)) >>> 0;
  3842. // 检查这个位置是否可能是有效数据
  3843. if (testValue > 0 && testValue <= 0x7FFFFFFF) {
  3844. console.log(`[数据恢复] 在位置 ${i} 找到可能的有效数据边界`);
  3845. chartDataBuffer = chartDataBuffer.slice(i);
  3846. return true;
  3847. }
  3848. }
  3849. return false;
  3850. }
  3851. // 旧的handleRealtimeHexData函数已删除,现在使用Worker方式处理数据
  3852. // ... existing code ...
  3853. function setupTerminalHexChartSync() {
  3854. // 这个函数现在不再需要,因为数据直接在handleReceivedData中处理
  3855. // 保留函数以避免调用错误,但不执行任何操作
  3856. console.log('[曲线同步] 已禁用终端监控同步,数据直接通过串口处理');
  3857. }
  3858. // ... existing code ...
  3859. // ... existing code ...
  3860. // 全屏样式
  3861. (function(){
  3862. const style = document.createElement('style');
  3863. style.innerHTML += `\n.realtime-chart-fullscreen {\n position: fixed !important;\n top: 0; left: 0; right: 0; bottom: 0;\n z-index: 9999;\n background: #fff;\n margin: 0 !important;\n padding: 20px !important;\n border-radius: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n box-shadow: 0 0 0 9999px rgba(0,0,0,0.15);\n display: flex;\n flex-direction: column;\n align-items: stretch;\n justify-content: flex-start;\n}\n.realtime-chart-fullscreen #realtimeChart {\n flex: 1;\n height: auto !important;\n min-height: 0 !important;\n}`;
  3864. document.head.appendChild(style);
  3865. })();
  3866. // ... existing code ...
  3867. // ... existing code ...
  3868. // ... existing code ...
  3869. // ... existing code ...
  3870. // 页面加载时初始化曲线图
  3871. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  3872. setTimeout(setupRealtimeChart, 300);
  3873. } else {
  3874. document.addEventListener('DOMContentLoaded', setupRealtimeChart);
  3875. }
  3876. // 立即绑定终止绘制按钮事件,确保按钮可用
  3877. function bindStopChartButton() {
  3878. const stopChartBtn = document.getElementById('stopChartBtn');
  3879. if (stopChartBtn) {
  3880. stopChartBtn.onclick = function() {
  3881. console.log('[终止绘制] 按钮被点击');
  3882. // 1. 清空曲线数据
  3883. window.chartData = [];
  3884. window.chartTimeData = []; // 清除时间数据
  3885. window.chartDataBuffer = []; // 清除缓冲区
  3886. // 2. 停止曲线绘制
  3887. if (window.chartDrawEnabled) {
  3888. window.chartDrawEnabled = false;
  3889. // 更新按钮状态
  3890. const startBtn = document.getElementById('startChartBtn');
  3891. if (startBtn) {
  3892. startBtn.textContent = '开始绘制';
  3893. startBtn.className = 'btn btn-success';
  3894. }
  3895. // 停止Worker
  3896. if (window.chartDataWorker) {
  3897. window.chartDataWorker.postMessage({ type: 'stop' });
  3898. }
  3899. if (window.chartRenderWorker) {
  3900. window.chartRenderWorker.postMessage({ type: 'stop' });
  3901. }
  3902. console.log('[终止绘制] 曲线绘制已停止');
  3903. }
  3904. // 3. 清空多变量图表
  3905. if (window.multiChartManager) {
  3906. window.multiChartManager.clearAllCharts();
  3907. }
  3908. // 4. 清空图表显示
  3909. if (window.chartInstance) {
  3910. window.chartInstance.setOption({
  3911. series: [{
  3912. data: []
  3913. }]
  3914. });
  3915. }
  3916. // 5. 发送终止绘制命令(如果串口已连接)
  3917. const stopCommand = 'cmd.read_ram(0x20000000,1,0)';
  3918. if (window.microLinkTerminal && window.microLinkTerminal.isConnected) {
  3919. window.microLinkTerminal.sendCommand(stopCommand);
  3920. console.log('[终止绘制] 已发送停止命令:', stopCommand);
  3921. } else {
  3922. console.log('[终止绘制] 串口未连接,跳过发送命令');
  3923. }
  3924. // 6. 关闭监听框的HEX模式
  3925. const hexModeCheckbox = document.getElementById('hexMode');
  3926. if (hexModeCheckbox && hexModeCheckbox.checked) {
  3927. hexModeCheckbox.checked = false;
  3928. hexModeCheckbox.dispatchEvent(new Event('change'));
  3929. console.log('[终止绘制] 已关闭HEX模式');
  3930. }
  3931. console.log('[终止绘制] 操作完成 - 已清空曲线并停止绘制');
  3932. };
  3933. console.log('[终止绘制] 按钮事件已绑定');
  3934. } else {
  3935. console.log('[终止绘制] 按钮未找到,将在setupRealtimeChart中绑定');
  3936. }
  3937. }
  3938. // 立即尝试绑定按钮
  3939. bindStopChartButton();
  3940. // 如果DOM还没准备好,等待后再次尝试
  3941. if (document.readyState !== 'complete') {
  3942. document.addEventListener('DOMContentLoaded', bindStopChartButton);
  3943. }
  3944. // 切换到变量分析tab时也初始化曲线图,防止切换后全局变量丢失
  3945. function setupVarPanelRealtimeChartInit() {
  3946. setTimeout(setupRealtimeChart, 200);
  3947. }
  3948. const varPanelBtn = document.querySelector('.sidebar-btn[data-panel="varPanel"]');
  3949. if (varPanelBtn) {
  3950. varPanelBtn.addEventListener('click', setupVarPanelRealtimeChartInit);
  3951. }
  3952. // ... existing code ...
  3953. // ... existing code ...
  3954. // 删除旧的串口发送区逻辑,因为HTML结构已经改变
  3955. // ... existing code ...
  3956. // ... existing code ...
  3957. // 删除重复的开始绘制按钮逻辑,已在setupRealtimeChart中处理
  3958. // ... existing code ...
  3959. // 获取主终端输出区域
  3960. function appendToTerminalOutput(html) {
  3961. const terminalOutput = document.getElementById('terminalOutput');
  3962. if (terminalOutput) {
  3963. terminalOutput.insertAdjacentHTML('beforeend', html);
  3964. terminalOutput.scrollTop = terminalOutput.scrollHeight;
  3965. }
  3966. }
  3967. // 删除重复的全局日志函数定义
  3968. // test.html的简化数据处理逻辑
  3969. function parseSerialData(buffer) {
  3970. const view = new Uint8Array(buffer);
  3971. const values = [];
  3972. // 每个字节作为一个独立的十进制值
  3973. for (let i = 0; i < view.length; i++) {
  3974. values.push(view[i]);
  3975. }
  3976. return values;
  3977. }
  3978. function processSerialBuffer() {
  3979. // 如果缓冲区有足够的数据,按4字节一组处理
  3980. while (chartDataBuffer.length >= 4) {
  3981. const group = chartDataBuffer.splice(0, 4);
  3982. console.log('处理4字节组:', group);
  3983. // 将4字节组合成32位整数 (小端序 - 从后往前读)
  3984. const value = group[0] | (group[1] << 8) | (group[2] << 16) | (group[3] << 24);
  3985. console.log('32位整数值 (小端序):', value);
  3986. // 发送数据到Worker - 添加时间戳,完全采用test.html的方式
  3987. if (chartDataWorker) {
  3988. const timestamp = performance.now();
  3989. chartDataWorker.postMessage({
  3990. type: 'serial_data',
  3991. values: [value],
  3992. timestamp: timestamp
  3993. });
  3994. }
  3995. }
  3996. }
  3997. // 新的简化版本,支持AA和55数据帧头帧尾解析
  3998. function handleRealtimeHexDataChunkedSimple(data) {
  3999. if (!chartDrawEnabled) return;
  4000. if (!(data instanceof Uint8Array)) return;
  4001. // 将新数据添加到缓冲区
  4002. if (!Array.isArray(chartDataBuffer)) {
  4003. chartDataBuffer = [];
  4004. }
  4005. chartDataBuffer.push(...Array.from(data));
  4006. // console.log('接收到串口数据块:', data.length, '字节, 缓冲区大小:', chartDataBuffer.length);
  4007. // 查找并解析AA和55之间的数据帧
  4008. parseDataFrames();
  4009. }
  4010. // 解析AA和55之间的数据帧
  4011. function parseDataFrames() {
  4012. const frameStart = 0xAA; // 帧头
  4013. const frameEnd = 0x55; // 帧尾
  4014. while (chartDataBuffer.length >= 2) {
  4015. // 查找帧头AA
  4016. const startIndex = chartDataBuffer.indexOf(frameStart);
  4017. if (startIndex === -1) {
  4018. // 没有找到帧头,清空缓冲区
  4019. chartDataBuffer.splice(0, chartDataBuffer.length);
  4020. return;
  4021. }
  4022. // 查找帧尾55(从帧头之后开始查找)
  4023. const endIndex = chartDataBuffer.indexOf(frameEnd, startIndex + 1);
  4024. if (endIndex === -1) {
  4025. // 没有找到帧尾,保留从帧头开始的数据,等待更多数据
  4026. chartDataBuffer.splice(0, startIndex);
  4027. return;
  4028. }
  4029. // 提取帧头帧尾之间的数据
  4030. const frameData = chartDataBuffer.slice(startIndex + 1, endIndex);
  4031. // console.log('找到数据帧:', {
  4032. // start: startIndex,
  4033. // end: endIndex,
  4034. // frameLength: frameData.length,
  4035. // frameData: Array.from(frameData).map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase())
  4036. // });
  4037. // 处理提取的数据帧
  4038. processDataFrame(frameData);
  4039. // 移除已处理的数据(包括帧头、帧尾和中间数据)
  4040. chartDataBuffer.splice(0, endIndex + 1);
  4041. }
  4042. }
  4043. // 处理提取的数据帧
  4044. function processDataFrame(frameData) {
  4045. if (frameData.length === 0) return;
  4046. // console.log('处理数据帧:', frameData.length, '字节:', Array.from(frameData).map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase()));
  4047. // 检查是否有选中的变量
  4048. if (!window.selectedVariables || window.selectedVariables.size === 0) {
  4049. console.log('没有选中的变量,跳过数据处理');
  4050. return;
  4051. }
  4052. // 获取选中的变量列表
  4053. const selectedVars = Array.from(window.selectedVariables);
  4054. const maxVars = Math.min(selectedVars.length, 9); // 最多9个变量
  4055. console.log(`[数据处理] 开始处理数据帧:`, {
  4056. '帧长度': frameData.length,
  4057. '选中变量数量': selectedVars.length,
  4058. '最大处理数量': maxVars,
  4059. '选中变量': selectedVars,
  4060. '数据内容': Array.from(frameData).map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase())
  4061. });
  4062. // 根据变量大小动态解析数据
  4063. let dataIndex = 0;
  4064. let frameIndex = 0;
  4065. // 特殊处理:如果数据帧很小,调整处理策略
  4066. if (frameData.length === 1) {
  4067. console.log(`[数据处理] 检测到1字节数据帧,启用特殊处理模式`);
  4068. // 对于1字节数据帧,强制第一个变量为1字节
  4069. if (selectedVars.length > 0) {
  4070. const varName = selectedVars[0];
  4071. console.log(`[数据处理] 1字节模式:将变量 ${varName} 设置为1字节`);
  4072. }
  4073. }
  4074. while (frameIndex < frameData.length && dataIndex < maxVars) {
  4075. const varName = selectedVars[dataIndex];
  4076. // 获取变量的字节大小 - 优先使用选择的变量实际大小
  4077. let varSize = 4; // 默认4字节
  4078. // 优先从变量信息中获取实际大小
  4079. if (window.variableInfo && window.variableInfo[varName]) {
  4080. varSize = window.variableInfo[varName].size || 4;
  4081. console.log(`[数据处理] 使用变量 ${varName} 的实际大小: ${varSize}字节`);
  4082. } else {
  4083. // 如果无法获取变量大小,尝试从命令中推断
  4084. const remainingBytes = frameData.length - frameIndex;
  4085. // 特殊处理:如果数据帧只有1字节且是第一个变量,强制使用1字节
  4086. if (frameData.length === 1 && dataIndex === 0) {
  4087. varSize = 1;
  4088. console.log(`[数据处理] 1字节数据帧模式:强制变量 ${varName} 为1字节`);
  4089. } else if (remainingBytes >= 4) {
  4090. // 数据充足,可以使用4字节
  4091. varSize = 4;
  4092. } else if (remainingBytes >= 2) {
  4093. // 数据足够2字节
  4094. varSize = 2;
  4095. } else if (remainingBytes >= 1) {
  4096. // 只有1字节数据,强制使用1字节
  4097. varSize = 1;
  4098. console.log(`[数据处理] 数据不足,将变量 ${varName} 调整为1字节大小`);
  4099. } else {
  4100. // 没有数据了
  4101. console.log(`[数据处理] 没有剩余数据,跳过变量 ${varName}`);
  4102. break;
  4103. }
  4104. console.log(`[数据处理] 智能推断变量 ${varName} 大小: ${varSize}字节 (剩余数据: ${remainingBytes}字节)`);
  4105. }
  4106. // 检查剩余数据是否足够(这个检查现在由智能推断逻辑处理)
  4107. // 如果智能推断后仍然不足,记录警告但继续处理
  4108. if (frameData.length - frameIndex < varSize) {
  4109. console.warn(`[数据处理] 警告:变量 ${varName} 需要 ${varSize} 字节,但剩余 ${frameData.length - frameIndex} 字节,尝试调整大小`);
  4110. // 强制调整为可用大小
  4111. varSize = Math.min(varSize, frameData.length - frameIndex);
  4112. if (varSize <= 0) {
  4113. console.log(`[数据处理] 无法调整大小,跳过变量 ${varName}`);
  4114. break;
  4115. }
  4116. console.log(`[数据处理] 已将变量 ${varName} 大小调整为 ${varSize} 字节`);
  4117. }
  4118. // 提取指定大小的数据
  4119. const group = frameData.slice(frameIndex, frameIndex + varSize);
  4120. frameIndex += varSize;
  4121. // 根据大小解析数据 - 使用无符号整数(uint)处理
  4122. let value;
  4123. if (varSize === 1) {
  4124. value = group[0]; // 直接使用无符号8位整数 (0-255)
  4125. } else if (varSize === 2) {
  4126. value = group[0] | (group[1] << 8); // 小端序16位无符号整数 (0-65535)
  4127. } else if (varSize === 4) {
  4128. value = group[0] | (group[1] << 8) | (group[2] << 16) | (group[3] << 24); // 小端序32位无符号整数 (0-4294967295)
  4129. } else {
  4130. // 对于其他大小,转换为无符号数值
  4131. value = parseInt(Array.from(group).map(b => b.toString(16).padStart(2, '0')).join(''), 16);
  4132. }
  4133. console.log(`[数据处理] 变量 ${varName} (${varSize}字节):`, value, '原始字节:', group.map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase()));
  4134. // 特殊显示1字节变量的信息
  4135. if (varSize === 1) {
  4136. console.log(`[数据处理] 1字节变量 ${varName} 处理完成:`, {
  4137. '原始值': group[0],
  4138. '无符号值': value,
  4139. '范围': `${value >= 0 && value <= 255 ? '有效' : '超出范围'}`,
  4140. '二进制': '0b' + group[0].toString(2).padStart(8, '0')
  4141. });
  4142. }
  4143. // 发送数据到多变量图表管理器
  4144. if (window.multiChartManager && window.chartDrawEnabled) {
  4145. // 检查图表是否存在,如果不存在则跳过
  4146. if (window.multiChartManager.hasChart(varName)) {
  4147. console.log(`[数据处理] 更新图表 - 变量: ${varName}, 值: ${value}`);
  4148. window.multiChartManager.updateChartData(varName, [value]);
  4149. } else {
  4150. console.log(`[数据处理] 跳过图表更新 - 变量 ${varName} 的图表不存在,尝试创建图表`);
  4151. // 尝试创建图表
  4152. window.multiChartManager.createChart(varName);
  4153. // 延迟更新数据
  4154. setTimeout(() => {
  4155. if (window.multiChartManager.hasChart(varName)) {
  4156. window.multiChartManager.updateChartData(varName, [value]);
  4157. }
  4158. }, 100);
  4159. }
  4160. }
  4161. dataIndex++;
  4162. }
  4163. // 如果剩余数据不足4字节,记录日志
  4164. if (frameData.length > 0) {
  4165. // console.log('数据帧剩余不足4字节的数据:', frameData.length, '字节:', Array.from(frameData).map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase()));
  4166. }
  4167. }
  4168. // 多变量图表管理器
  4169. class MultiChartManager {
  4170. constructor() {
  4171. this.container = null;
  4172. this.charts = new Map(); // 存储变量名到图表实例的映射
  4173. this.chartData = new Map(); // 存储变量名到数据的映射
  4174. this.chartTimeData = new Map(); // 存储变量名到时间数据的映射
  4175. this.varTimestamps = new Map(); // 存储每个变量的时间戳序列
  4176. this.defaultChart = null;
  4177. this.samplingRate = 50; // 默认采样率50Hz
  4178. this.timeDisplayRange = 10; // 默认时间显示范围10秒
  4179. this.debugMode = false; // 调试模式开关,默认关闭
  4180. this.lastUpdateTime = new Map(); // 每个变量的上次更新时间,用于节流绘制
  4181. console.log(`[MultiChartManager] 初始化完成 - 默认采样率: ${this.samplingRate}Hz, 显示范围: ${this.timeDisplayRange}秒`);
  4182. // 延迟初始化,等待DOM元素创建完成
  4183. this.init();
  4184. }
  4185. init() {
  4186. this.container = document.getElementById('multiChartContainer');
  4187. this.defaultChart = document.getElementById('realtimeChart');
  4188. if (!this.container) {
  4189. console.error('多变量图表管理器容器未找到,将在100ms后重试');
  4190. setTimeout(() => this.init(), 100);
  4191. return;
  4192. }
  4193. console.log('多变量图表管理器已初始化');
  4194. }
  4195. /**
  4196. * 为指定变量创建图表
  4197. */
  4198. createChart(varName) {
  4199. console.log(`[MultiChartManager] 尝试创建图表: ${varName}`);
  4200. if (this.charts.has(varName)) {
  4201. console.log(`[MultiChartManager] 变量 ${varName} 的图表已存在,跳过创建`);
  4202. return;
  4203. }
  4204. // 检查容器是否准备好
  4205. if (!this.container) {
  4206. console.log(`[MultiChartManager] 容器未准备好,延迟创建图表: ${varName}`);
  4207. setTimeout(() => this.createChart(varName), 100);
  4208. return;
  4209. }
  4210. console.log(`[MultiChartManager] 开始创建图表: ${varName}, 容器状态:`, !!this.container);
  4211. // 隐藏默认图表
  4212. if (this.defaultChart) {
  4213. this.defaultChart.style.display = 'none';
  4214. }
  4215. // 创建图表容器
  4216. const chartDiv = document.createElement('div');
  4217. chartDiv.className = 'variable-chart';
  4218. chartDiv.setAttribute('data-var', varName);
  4219. chartDiv.innerHTML = `
  4220. <div class="variable-chart-header">
  4221. <div class="variable-chart-title">${varName}</div>
  4222. <div class="variable-chart-controls">
  4223. <button class="btn btn-sm btn-outline-info" onclick="window.multiChartManager.fullscreenChart('${varName}')" title="全屏显示">
  4224. <span style="font-size:12px;">⛶</span>
  4225. </button>
  4226. </div>
  4227. </div>
  4228. <div class="variable-chart-content" style="width:100%;height:400px;"></div>
  4229. `;
  4230. // 调试:显示图表容器信息
  4231. console.log(`[MultiChartManager] 创建图表容器 - 变量 ${varName}:`, {
  4232. '容器类名': chartDiv.className,
  4233. '容器属性': chartDiv.getAttribute('data-var'),
  4234. '容器HTML': chartDiv.innerHTML.substring(0, 100) + '...',
  4235. '容器高度': chartDiv.querySelector('.variable-chart-content').style.height
  4236. });
  4237. // 确保容器存在且可访问
  4238. if (!this.container || !this.container.appendChild) {
  4239. console.error(`[MultiChartManager] 容器不可用,无法创建图表 - 变量 ${varName}`);
  4240. return;
  4241. }
  4242. this.container.appendChild(chartDiv);
  4243. // 初始化ECharts实例
  4244. const chartDom = chartDiv.querySelector('.variable-chart-content');
  4245. let chartInstance;
  4246. try {
  4247. chartInstance = echarts.init(chartDom);
  4248. console.log(`[MultiChartManager] ECharts实例创建成功 - 变量 ${varName}`);
  4249. } catch (error) {
  4250. console.error(`[MultiChartManager] ECharts实例创建失败 - 变量 ${varName}:`, error);
  4251. // 移除失败的DOM元素
  4252. chartDiv.remove();
  4253. return;
  4254. }
  4255. // 设置图表配置,与默认图表保持一致
  4256. try {
  4257. chartInstance.setOption({
  4258. xAxis: {
  4259. type: 'value',
  4260. name: '时间 (秒)',
  4261. nameLocation: 'middle',
  4262. nameGap: 30,
  4263. min: 0,
  4264. max: this.timeDisplayRange, // 使用动态监测时间
  4265. axisLabel: {
  4266. formatter: function(value) {
  4267. return value.toFixed(1) + 's';
  4268. }
  4269. }
  4270. },
  4271. yAxis: {
  4272. type: 'value',
  4273. scale: true,
  4274. name: '数值',
  4275. nameLocation: 'middle',
  4276. nameGap: 40,
  4277. min: -130,
  4278. max: 130,
  4279. axisLabel: {
  4280. formatter: function(value) {
  4281. // 如果是1字节范围,显示整数
  4282. if (value >= -128 && value <= 127) {
  4283. return Math.round(value);
  4284. }
  4285. return value.toFixed(1);
  4286. }
  4287. }
  4288. },
  4289. series: [{
  4290. type: 'line',
  4291. data: [],
  4292. smooth: true,
  4293. symbol: 'none',
  4294. lineStyle: { width: 2, color: '#3498db' },
  4295. name: '实时数据'
  4296. }],
  4297. grid: {
  4298. left: 80,
  4299. right: 40,
  4300. top: 50,
  4301. bottom: 80,
  4302. containLabel: true
  4303. },
  4304. animation: false,
  4305. tooltip: {
  4306. trigger: 'axis',
  4307. formatter: function(params) {
  4308. const data = params[0];
  4309. const timeStr = params[0].axisValue || '未知时间';
  4310. // 确保数值正确显示
  4311. let valueStr;
  4312. if (Array.isArray(data.value)) {
  4313. // 如果data.value是数组[time, value],取第二个元素作为数值
  4314. valueStr = data.value[1];
  4315. } else {
  4316. // 如果data.value是单个数值
  4317. valueStr = data.value;
  4318. }
  4319. return `时间: ${timeStr}<br/>数值: ${valueStr}`;
  4320. }
  4321. }
  4322. });
  4323. } catch (error) {
  4324. console.error(`[MultiChartManager] 图表配置设置失败 - 变量 ${varName}:`, error);
  4325. // 销毁失败的实例并移除DOM
  4326. chartInstance.dispose();
  4327. chartDiv.remove();
  4328. return;
  4329. }
  4330. // 存储图表实例和数据
  4331. this.charts.set(varName, chartInstance);
  4332. this.chartData.set(varName, []);
  4333. // 初始化时间戳数组
  4334. this.varTimestamps.set(varName, []);
  4335. // 初始化该变量的更新时间戳
  4336. this.lastUpdateTime.set(varName, 0);
  4337. // 为Y轴范围计算提供chartData引用
  4338. chartInstance.chartData = this.chartData.get(varName);
  4339. // 强制resize
  4340. setTimeout(() => {
  4341. chartInstance.resize();
  4342. }, 100);
  4343. console.log(`[MultiChartManager] 为变量 ${varName} 创建了图表:`, {
  4344. '当前图表数量': this.charts.size,
  4345. '图表实例': !!chartInstance,
  4346. '图表DOM': !!chartDom,
  4347. '数据数组': this.chartData.get(varName),
  4348. '时间戳数组': this.varTimestamps.get(varName),
  4349. '容器子元素数量': this.container.children.length,
  4350. '图表容器可见性': chartDiv.style.display,
  4351. '图表内容高度': chartDom.style.height,
  4352. '图表容器位置': chartDiv.offsetTop + 'px'
  4353. });
  4354. }
  4355. /**
  4356. * 检查指定变量是否有图表
  4357. */
  4358. hasChart(varName) {
  4359. const hasChart = this.charts.has(varName);
  4360. const chartInstance = this.charts.get(varName);
  4361. const chartData = this.chartData.get(varName);
  4362. console.log(`[MultiChartManager] 检查图表 ${varName}:`, {
  4363. 'hasChart': hasChart,
  4364. 'chartInstance': !!chartInstance,
  4365. 'chartData': !!chartData,
  4366. '数据长度': chartData ? chartData.length : 0
  4367. });
  4368. return hasChart;
  4369. }
  4370. /**
  4371. * 更新指定变量的图表数据
  4372. */
  4373. updateChartData(varName, newData) {
  4374. // 检查容器是否准备好
  4375. if (!this.container) {
  4376. console.log(`[MultiChartManager] 容器未准备好,延迟更新数据: ${varName}`);
  4377. setTimeout(() => this.updateChartData(varName, newData, timestamp), 100);
  4378. return;
  4379. }
  4380. const chartInstance = this.charts.get(varName);
  4381. const chartData = this.chartData.get(varName);
  4382. if (!chartInstance || !chartData) {
  4383. // console.log(`[MultiChartManager] 变量 ${varName} 的图表或数据未找到 - chartInstance:`, !!chartInstance, 'chartData:', !!chartData);
  4384. return;
  4385. }
  4386. // console.log(`[MultiChartManager] 更新变量 ${varName} 的数据:`, newData, '当前数据长度:', chartData.length);
  4387. // 环形缓冲区:固定大小,自动覆盖旧数据
  4388. const bufferSize = 6000;
  4389. // 添加新数据到数组
  4390. chartData.push(...newData);
  4391. // 如果数据超过缓冲区大小,自动移除最旧的数据
  4392. if (chartData.length > bufferSize) {
  4393. const removeCount = chartData.length - bufferSize;
  4394. chartData.splice(0, removeCount);
  4395. }
  4396. // 更新图表实例的chartData引用,确保Y轴范围计算能正常工作
  4397. chartInstance.chartData = chartData;
  4398. // 调试:显示数据累积状态
  4399. console.log(`[MultiChartManager] 数据累积 - 变量 ${varName}:`, {
  4400. '新数据': newData,
  4401. '累积后长度': chartData.length,
  4402. '缓冲区大小': bufferSize,
  4403. '数据示例': chartData.slice(-3) // 显示最后3个数据点
  4404. });
  4405. // 计算时间数据,使用真实的时间戳
  4406. let timeData;
  4407. // 为每个数据点生成时间戳(如果没有时间戳数组,则创建)
  4408. if (!this.varTimestamps.has(varName)) {
  4409. this.varTimestamps.set(varName, []);
  4410. }
  4411. const timestamps = this.varTimestamps.get(varName);
  4412. // 为每个新数据点添加时间戳
  4413. for (let i = 0; i < newData.length; i++) {
  4414. const currentTime = performance.now();
  4415. timestamps.push(currentTime);
  4416. }
  4417. // 限制时间戳数量,与数据保持一致
  4418. if (timestamps.length > bufferSize) {
  4419. const removeCount = timestamps.length - bufferSize;
  4420. timestamps.splice(0, removeCount);
  4421. }
  4422. // 计算相对时间(只显示可视区间内的数据)
  4423. if (timestamps.length > 0) {
  4424. const currentTime = performance.now();
  4425. const visibleStartTime = currentTime - (this.timeDisplayRange * 1000); // 可视区间的开始时间
  4426. // 只保留可视区间内的数据点
  4427. const visibleTimestamps = timestamps.filter(ts => ts >= visibleStartTime);
  4428. const visibleChartData = chartData.slice(-visibleTimestamps.length);
  4429. // 计算相对时间(从可视区间开始时间开始)
  4430. timeData = visibleTimestamps.map(ts => (ts - visibleStartTime) / 1000);
  4431. // 更新图表数据为可视区间内的数据
  4432. chartData.length = 0;
  4433. chartData.push(...visibleChartData);
  4434. // 更新时间戳为可视区间内的时间戳
  4435. timestamps.length = 0;
  4436. timestamps.push(...visibleTimestamps);
  4437. } else {
  4438. timeData = [];
  4439. }
  4440. // 调试信息:显示时间数据(限制频率)
  4441. // if (timeData.length > 1 && timeData.length % 50 === 0) {
  4442. // const lastTwoTime = timeData.slice(-2);
  4443. // console.log(`[MultiChartManager] 变量 ${varName} 时间数据:`,
  4444. // `前一个: ${lastTwoTime[0].toFixed(3)}s, 当前: ${lastTwoTime[1].toFixed(3)}s, 间隔: ${(lastTwoTime[1] - lastTwoTime[0]).toFixed(3)}s`);
  4445. // }
  4446. // 调试信息:显示滑动窗口信息(限制频率)
  4447. // if (timestamps.length > 1000 && timestamps.length % 100 === 0) {
  4448. // const windowStart = timestamps[0];
  4449. // const windowEnd = timestamps[timestamps.length - 1];
  4450. // const windowDuration = (windowEnd - windowStart) / 1000;
  4451. // console.log(`[MultiChartManager] 变量 ${varName} 滑动窗口:`,
  4452. // `起始: ${(windowStart / 1000).toFixed(3)}s, 结束: ${(windowEnd / 1000).toFixed(3)}s, 窗口长度: ${windowDuration.toFixed(3)}s`);
  4453. // }
  4454. // 更新图表 - 使用批量更新和节流绘制
  4455. // 确保数据格式正确,避免白屏
  4456. if (!timeData || timeData.length === 0 || !chartData || chartData.length === 0) {
  4457. console.warn(`[MultiChartManager] 数据不完整,跳过图表更新 - 变量 ${varName}:`, {
  4458. 'timeData长度': timeData ? timeData.length : 0,
  4459. 'chartData长度': chartData ? chartData.length : 0
  4460. });
  4461. return;
  4462. }
  4463. const chartDataArray = chartData.map((value, index) => [timeData[index], value]);
  4464. // 调试:显示数据格式
  4465. console.log(`[MultiChartManager] 图表数据准备 - 变量 ${varName}:`, {
  4466. '数据点数量': timeData.length,
  4467. '数据数组长度': chartData.length,
  4468. '时间数据长度': timeData.length,
  4469. '图表数据数组': chartDataArray.slice(-3), // 显示最后3个数据点
  4470. '时间范围': timeData.length > 0 ? `${Math.min(...timeData).toFixed(3)}s - ${Math.max(...timeData).toFixed(3)}s` : '无'
  4471. });
  4472. // 节流绘制:限制更新频率,避免1000Hz时卡死
  4473. const now = performance.now();
  4474. const lastUpdateTime = this.lastUpdateTime.get(varName) || 0;
  4475. const updateInterval = Math.max(50, 1000 / Math.min(this.samplingRate, 60)); // 最少50ms更新一次,最多60Hz
  4476. console.log(`[MultiChartManager] 节流检查 - 变量 ${varName}:`, {
  4477. '当前时间': now.toFixed(1),
  4478. '上次更新时间': lastUpdateTime.toFixed(1),
  4479. '时间差': (now - lastUpdateTime).toFixed(1) + 'ms',
  4480. '更新间隔': updateInterval.toFixed(1) + 'ms',
  4481. '是否允许更新': (now - lastUpdateTime >= updateInterval)
  4482. });
  4483. if (now - lastUpdateTime >= updateInterval) {
  4484. // 使用增量更新,只更新数据,不重新配置整个图表
  4485. const option = {
  4486. series: [{
  4487. data: chartDataArray,
  4488. type: 'line',
  4489. smooth: true,
  4490. symbol: 'none',
  4491. lineStyle: { width: 2, color: '#3498db' },
  4492. name: '实时数据'
  4493. }],
  4494. // 确保包含所有必要的配置,避免白屏
  4495. animation: false,
  4496. // 确保网格配置存在
  4497. grid: {
  4498. left: 80,
  4499. right: 40,
  4500. top: 50,
  4501. bottom: 80,
  4502. containLabel: true
  4503. },
  4504. // 保持X轴配置,但更新数据范围
  4505. xAxis: {
  4506. type: 'value',
  4507. name: '时间 (秒)',
  4508. nameLocation: 'middle',
  4509. nameGap: 30,
  4510. // 设置X轴范围,固定显示0到timeDisplayRange
  4511. min: 0,
  4512. max: this.timeDisplayRange,
  4513. // 启用时间轴滚动
  4514. axisLabel: {
  4515. formatter: function(value) {
  4516. return value.toFixed(1) + 's';
  4517. }
  4518. }
  4519. },
  4520. // 保持Y轴配置
  4521. yAxis: {
  4522. type: 'value',
  4523. scale: true,
  4524. name: '数值',
  4525. nameLocation: 'middle',
  4526. nameGap: 40,
  4527. // 动态计算Y轴范围
  4528. min: (() => {
  4529. if (chartData.length > 0) {
  4530. const minVal = Math.min(...chartData);
  4531. const maxVal = Math.max(...chartData);
  4532. const range = maxVal - minVal;
  4533. // 如果是1字节数据,设置合适的范围
  4534. if (minVal >= -128 && maxVal <= 127) {
  4535. return Math.max(-130, minVal - 2);
  4536. }
  4537. // 对于大数值,使用更合理的余量计算
  4538. if (range > 0) {
  4539. // 使用数据范围的5%作为余量,但最小1,最大1000
  4540. const margin = Math.max(1, Math.min(1000, range * 0.05));
  4541. return minVal - margin;
  4542. } else {
  4543. // 如果数据没有变化,使用固定余量
  4544. return minVal - Math.abs(minVal) * 0.01;
  4545. }
  4546. }
  4547. return 0;
  4548. })(),
  4549. max: (() => {
  4550. if (chartData.length > 0) {
  4551. const minVal = Math.min(...chartData);
  4552. const maxVal = Math.max(...chartData);
  4553. const range = maxVal - minVal;
  4554. // 如果是1字节数据,设置合适的范围
  4555. if (minVal >= -128 && maxVal <= 127) {
  4556. return Math.min(130, maxVal + 2);
  4557. }
  4558. // 对于大数值,使用更合理的余量计算
  4559. if (range > 0) {
  4560. // 使用数据范围的5%作为余量,但最小1,最大1000
  4561. const margin = Math.max(1, Math.min(1000, range * 0.05));
  4562. return maxVal + margin;
  4563. } else {
  4564. // 如果数据没有变化,使用固定余量
  4565. return maxVal + Math.abs(maxVal) * 0.01;
  4566. }
  4567. }
  4568. return 100;
  4569. })(),
  4570. // 动态设置Y轴标签格式
  4571. axisLabel: {
  4572. formatter: function(value) {
  4573. // 如果是1字节范围,显示整数
  4574. if (value >= -128 && value <= 127) {
  4575. return Math.round(value);
  4576. }
  4577. // 对于大数值,使用更简洁的格式
  4578. if (Math.abs(value) >= 1000) {
  4579. return value.toFixed(0);
  4580. }
  4581. // 对于中等数值,保留1位小数
  4582. if (Math.abs(value) >= 10) {
  4583. return value.toFixed(1);
  4584. }
  4585. // 对于小数值,保留2位小数
  4586. return value.toFixed(2);
  4587. }
  4588. }
  4589. },
  4590. // 保持网格配置
  4591. grid: {
  4592. left: 80,
  4593. right: 40,
  4594. top: 50,
  4595. bottom: 80,
  4596. containLabel: true
  4597. },
  4598. // 保持工具提示配置
  4599. tooltip: {
  4600. trigger: 'axis',
  4601. formatter: function(params) {
  4602. const data = params[0];
  4603. const timeStr = params[0].axisValue || '未知时间';
  4604. // 确保数值正确显示
  4605. let valueStr;
  4606. if (Array.isArray(data.value)) {
  4607. // 如果data.value是数组[time, value],取第二个元素作为数值
  4608. valueStr = data.value[1];
  4609. } else {
  4610. // 如果data.value是单个数值
  4611. valueStr = data.value;
  4612. }
  4613. return `时间: ${timeStr}<br/>数值: ${valueStr}`;
  4614. }
  4615. },
  4616. // 确保图表类型配置存在
  4617. type: 'line'
  4618. };
  4619. // 使用增量更新,提高性能
  4620. try {
  4621. chartInstance.setOption(option, true);
  4622. console.log(`[MultiChartManager] 图表更新成功 - 变量 ${varName}:`, {
  4623. '选项配置': option,
  4624. '图表实例状态': !!chartInstance,
  4625. '图表DOM状态': !!chartInstance.getDom()
  4626. });
  4627. } catch (error) {
  4628. console.error(`[MultiChartManager] 图表更新失败 - 变量 ${varName}:`, error);
  4629. // 如果增量更新失败,尝试完全重新设置
  4630. try {
  4631. chartInstance.setOption(option, false);
  4632. console.log(`[MultiChartManager] 图表重新设置成功 - 变量 ${varName}`);
  4633. } catch (retryError) {
  4634. console.error(`[MultiChartManager] 图表重新设置也失败 - 变量 ${varName}:`, retryError);
  4635. }
  4636. }
  4637. // 记录更新时间(为每个变量单独记录)
  4638. this.lastUpdateTime.set(varName, now);
  4639. // 调试:显示绘制结果
  4640. console.log(`[MultiChartManager] 图表更新完成 - 变量 ${varName}:`, {
  4641. '数据点数量': timeData.length,
  4642. '更新间隔': updateInterval.toFixed(1) + 'ms',
  4643. '采样率': this.samplingRate + 'Hz',
  4644. '绘制频率': (1000 / updateInterval).toFixed(1) + 'Hz',
  4645. '图表实例状态': !!chartInstance,
  4646. '图表数据长度': chartInstance.getOption().series[0].data.length,
  4647. '容器子元素数量': this.container.children.length,
  4648. '图表DOM元素': !!this.container.querySelector(`[data-var="${varName}"]`)
  4649. });
  4650. }
  4651. // 调试信息:显示X轴范围(限制频率,避免过多日志)
  4652. // if (timeData.length > 0 && timeData.length % 100 === 0) { // 每100个数据点输出一次
  4653. // const xMin = Math.min(...timeData);
  4654. // const xMax = Math.max(...timeData);
  4655. // console.log(`[MultiChartManager] 变量 ${varName} 图表更新完成,数据点: ${timeData.length}, X轴范围: ${xMin.toFixed(3)}s - ${xMax.toFixed(3)}s`);
  4656. // // 显示实际时间范围变化
  4657. // if (timestamps.length > 1) {
  4658. // const firstTime = (timestamps[0] / 1000).toFixed(3);
  4659. // const lastTime = (timestamps[timestamps.length - 1] / 1000).toFixed(3);
  4660. // console.log(`[MultiChartManager] 变量 ${varName} 图表更新完成,数据点: ${timeData.length}, X轴范围: ${xMin.toFixed(3)}s - ${xMax.toFixed(3)}s`);
  4661. // }
  4662. // }
  4663. }
  4664. /**
  4665. * 删除指定变量的图表
  4666. */
  4667. removeChart(varName) {
  4668. const chartInstance = this.charts.get(varName);
  4669. if (chartInstance) {
  4670. chartInstance.dispose();
  4671. }
  4672. this.charts.delete(varName);
  4673. this.chartData.delete(varName);
  4674. this.chartTimeData.delete(varName);
  4675. this.varTimestamps.delete(varName);
  4676. // 移除DOM元素
  4677. const chartElement = this.container.querySelector(`[data-var="${varName}"]`);
  4678. if (chartElement) {
  4679. chartElement.remove();
  4680. }
  4681. // 如果没有其他图表,显示默认图表
  4682. if (this.charts.size === 0 && this.defaultChart) {
  4683. this.defaultChart.style.display = 'block';
  4684. }
  4685. // console.log(`删除了变量 ${varName} 的图表`);
  4686. }
  4687. /**
  4688. * 清空所有图表
  4689. */
  4690. clearAllCharts() {
  4691. // 销毁所有图表实例
  4692. this.charts.forEach((chartInstance, varName) => {
  4693. chartInstance.dispose();
  4694. });
  4695. this.charts.clear();
  4696. this.chartData.clear();
  4697. this.chartTimeData.clear();
  4698. this.varTimestamps.clear();
  4699. // 清空容器中的所有图表DOM元素
  4700. const chartElements = this.container.querySelectorAll('.variable-chart');
  4701. chartElements.forEach(element => {
  4702. element.remove();
  4703. });
  4704. // 确保默认图表可见
  4705. if (this.defaultChart) {
  4706. this.defaultChart.style.display = 'block';
  4707. }
  4708. // console.log('已清空所有多变量图表');
  4709. }
  4710. /**
  4711. * 设置采样率
  4712. */
  4713. setSamplingRate(rate) {
  4714. this.samplingRate = rate;
  4715. console.log(`[MultiChartManager] 采样率设置为 ${rate}Hz`);
  4716. }
  4717. /**
  4718. * 设置时间显示范围
  4719. */
  4720. setTimeDisplayRange(range) {
  4721. this.timeDisplayRange = range;
  4722. console.log(`[MultiChartManager] 时间显示范围设置为 ${range}秒`);
  4723. // 立即更新所有图表的X轴范围
  4724. this.charts.forEach((chartInstance, varName) => {
  4725. if (chartInstance && chartInstance.getOption) {
  4726. const option = chartInstance.getOption();
  4727. const chartData = this.chartData.get(varName);
  4728. const timestamps = this.varTimestamps.get(varName);
  4729. if (chartData && timestamps && timestamps.length > 0) {
  4730. const startTime = timestamps[0];
  4731. const timeData = timestamps.map(ts => (ts - startTime) / 1000);
  4732. const maxTime = Math.max(...timeData);
  4733. // 更新X轴范围
  4734. option.xAxis = {
  4735. ...option.xAxis,
  4736. min: 0,
  4737. max: this.timeDisplayRange
  4738. };
  4739. chartInstance.setOption(option);
  4740. }
  4741. }
  4742. });
  4743. }
  4744. /**
  4745. * 全屏显示指定变量的图表
  4746. */
  4747. fullscreenChart(varName) {
  4748. const chartInstance = this.charts.get(varName);
  4749. if (!chartInstance) {
  4750. console.warn(`[MultiChartManager] 变量 ${varName} 的图表不存在`);
  4751. return;
  4752. }
  4753. // 获取图表DOM元素
  4754. const chartElement = this.container.querySelector(`[data-var="${varName}"]`);
  4755. if (!chartElement) {
  4756. console.warn(`[MultiChartManager] 变量 ${varName} 的图表DOM元素不存在`);
  4757. return;
  4758. }
  4759. // 创建全屏容器
  4760. const fullscreenContainer = document.createElement('div');
  4761. fullscreenContainer.id = 'fullscreenChartContainer';
  4762. fullscreenContainer.style.cssText = `
  4763. position: fixed;
  4764. top: 0;
  4765. left: 0;
  4766. width: 100vw;
  4767. height: 100vh;
  4768. background: rgba(0, 0, 0, 0.9);
  4769. z-index: 9999;
  4770. display: flex;
  4771. flex-direction: column;
  4772. align-items: center;
  4773. justify-content: center;
  4774. `;
  4775. // 创建全屏内容
  4776. fullscreenContainer.innerHTML = `
  4777. <div style="width: 95vw; height: 90vh; background: white; border-radius: 8px; position: relative;">
  4778. <div style="position: absolute; top: 10px; right: 10px; z-index: 10000;">
  4779. <button class="btn btn-danger btn-sm" onclick="document.getElementById('fullscreenChartContainer').remove()" style="font-size: 16px; padding: 8px 12px;">
  4780. ✕ 关闭全屏
  4781. </button>
  4782. </div>
  4783. <div id="fullscreenChartContent" style="width: 100%; height: 100%; padding: 20px;"></div>
  4784. </div>
  4785. `;
  4786. // 添加到页面
  4787. document.body.appendChild(fullscreenContainer);
  4788. // 创建新的ECharts实例
  4789. const fullscreenChartDom = document.getElementById('fullscreenChartContent');
  4790. const fullscreenChartInstance = echarts.init(fullscreenChartDom);
  4791. // 复制原图表的配置和数据
  4792. const originalOption = chartInstance.getOption();
  4793. fullscreenChartInstance.setOption(originalOption);
  4794. // 监听窗口大小变化,自动调整图表大小
  4795. const resizeHandler = () => {
  4796. fullscreenChartInstance.resize();
  4797. };
  4798. window.addEventListener('resize', resizeHandler);
  4799. // 监听全屏容器移除事件,清理事件监听器
  4800. const observer = new MutationObserver((mutations) => {
  4801. mutations.forEach((mutation) => {
  4802. if (mutation.type === 'childList') {
  4803. mutation.removedNodes.forEach((node) => {
  4804. if (node.id === 'fullscreenChartContainer') {
  4805. window.removeEventListener('resize', resizeHandler);
  4806. observer.disconnect();
  4807. }
  4808. });
  4809. }
  4810. });
  4811. });
  4812. observer.observe(document.body, { childList: true });
  4813. console.log(`[MultiChartManager] 变量 ${varName} 图表已全屏显示`);
  4814. }
  4815. }