| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498 |
- /**
- * MicroLink Web Serial Terminal
- * 使用 Web Serial API 实现浏览器直接访问串口
- */
- class MicroLinkTerminal {
- constructor() {
- this.port = null;
- this.reader = null;
- this.writer = null;
- this.isConnected = false;
- this.customCommands = [];
- // 数据缓冲相关
- this.dataBuffer = '';
- this.lineTimeoutId = null;
- this.bufferStartTime = null; // 记录缓冲区开始接收数据的时间
- // 终端输入相关
- this.commandHistory = [];
- this.historyIndex = -1;
- this.logMessages = [];
- // 虚拟终端配置
- this.virtualTerminalMode = true; // 启用虚拟终端模式,实时发送按键
- this.virtualTerminalModeOriginalValue = true; // 保存用户原始设置值
- this.currentLine = ''; // 当前行缓冲
- this.initializeElements();
- this.bindEvents();
- this.updateSendOptions(); // 初始化发送选项状态
- this.loadParameters();
- this.checkWebSerialSupport();
- }
- initializeElements() {
- // 串口配置元素
- this.baudRateSelect = document.getElementById('baudRate');
- this.customBaudRateInput = document.getElementById('customBaudRate');
- this.dataBitsSelect = document.getElementById('dataBits');
- this.paritySelect = document.getElementById('parity');
- this.stopBitsSelect = document.getElementById('stopBits');
- // 连接控制元素
- this.connectBtn = document.getElementById('connectBtn');
- this.disconnectBtn = document.getElementById('disconnectBtn');
- this.clearBtn = document.getElementById('clearBtn');
- this.saveLogBtn = document.getElementById('saveLogBtn');
- this.connectionStatus = document.getElementById('connectionStatus');
- // 显示选项元素
- this.hexModeCheckbox = document.getElementById('hexMode');
- this.showTimestampCheckbox = document.getElementById('showTimestamp');
- this.autoScrollCheckbox = document.getElementById('autoScroll');
- this.enableBufferCheckbox = document.getElementById('enableBuffer');
- this.virtualTerminalModeCheckbox = document.getElementById('virtualTerminalMode');
- this.processAnsiSequencesCheckbox = document.getElementById('processAnsiSequences');
- this.debugModeCheckbox = document.getElementById('debugMode');
- this.lineTimeoutInput = document.getElementById('lineTimeout');
- // 终端元素
- this.terminal = document.getElementById('terminal');
- this.terminalOutput = document.getElementById('terminalOutput');
- this.terminalInputField = document.getElementById('terminalInputField');
- // API 控制元素
- this.rttAddrInput = document.getElementById('rttAddr');
- this.rttSizeInput = document.getElementById('rttSize');
- this.rttChannelInput = document.getElementById('rttChannel');
- this.startRTTBtn = document.getElementById('startRTT');
- this.stopRTTBtn = document.getElementById('stopRTT');
- this.svAddrInput = document.getElementById('svAddr');
- this.svSizeInput = document.getElementById('svSize');
- this.svChannelInput = document.getElementById('svChannel');
- this.startSystemViewBtn = document.getElementById('startSystemView');
- this.flmPathInput = document.getElementById('flmPath');
- this.baseAddrInput = document.getElementById('baseAddr');
- this.ramAddrInput = document.getElementById('ramAddr');
- this.loadFLMBtn = document.getElementById('loadFLM');
- this.binPathInput = document.getElementById('binPath');
- this.binAddrInput = document.getElementById('binAddr');
- this.loadBinBtn = document.getElementById('loadBin');
- this.offlineDownloadBtn = document.getElementById('offlineDownload');
- this.ymodemFileInput = document.getElementById('ymodemFile');
- this.ymodemSendBtn = document.getElementById('ymodemSend');
- this.customCommandInput = document.getElementById('customCommand');
- this.sendCustomBtn = document.getElementById('sendCustom');
- this.addCustomBtn = document.getElementById('addCustom');
- this.customCommandsList = document.getElementById('customCommandsList');
- this.saveParamsBtn = document.getElementById('saveParams');
- this.saveToFileBtn = document.getElementById('saveToFile');
- this.loadParamsBtn = document.getElementById('loadParams');
- this.resetParamsBtn = document.getElementById('resetParams');
- this.loadConfigFileBtn = document.getElementById('loadConfigFile');
- this.configFileInput = document.getElementById('configFileInput');
- }
- bindEvents() {
- // 波特率选择事件
- this.baudRateSelect.addEventListener('change', () => {
- if (this.baudRateSelect.value === 'custom') {
- this.customBaudRateInput.style.display = 'block';
- } else {
- this.customBaudRateInput.style.display = 'none';
- }
- });
- // 连接控制事件
- this.connectBtn.addEventListener('click', () => this.connectSerial());
- this.disconnectBtn.addEventListener('click', () => this.disconnectSerial());
- this.clearBtn.addEventListener('click', () => this.clearTerminal());
- this.saveLogBtn.addEventListener('click', () => this.saveLog());
- // 终端输入事件
- this.terminalInputField.addEventListener('keydown', (e) => this.handleTerminalInput(e));
- this.terminalInputField.addEventListener('focus', () => this.scrollToBottom());
-
- // 终端发送按钮事件
- this.terminalSendBtn = document.getElementById('terminalSendBtn');
- if (this.terminalSendBtn) {
- this.terminalSendBtn.addEventListener('click', () => this.executeTerminalCommand());
- }
- // 虚拟终端配置事件
- this.virtualTerminalModeCheckbox.addEventListener('change', () => {
- this.virtualTerminalMode = this.virtualTerminalModeCheckbox.checked;
- // 只有在非HEX模式下才保存用户的原始设置
- if (!this.hexModeCheckbox.checked) {
- this.virtualTerminalModeOriginalValue = this.virtualTerminalModeCheckbox.checked;
- }
- this.updateTerminalPlaceholder();
- });
- // HEX模式切换事件
- this.hexModeCheckbox.addEventListener('change', () => {
- this.updateSendOptions();
- });
- // API 控制事件
- this.startRTTBtn.addEventListener('click', () => this.startRTT());
- this.stopRTTBtn.addEventListener('click', () => this.stopRTT());
- this.startSystemViewBtn.addEventListener('click', () => this.startSystemView());
- this.loadFLMBtn.addEventListener('click', () => this.loadFLM());
- this.loadBinBtn.addEventListener('click', () => this.loadBin());
- this.offlineDownloadBtn.addEventListener('click', () => this.offlineDownload());
- this.ymodemSendBtn.addEventListener('click', () => this.ymodemSend());
- this.sendCustomBtn.addEventListener('click', () => this.sendCustomCommand());
- this.addCustomBtn.addEventListener('click', () => this.addCustomCommand());
- // 参数管理事件
- this.saveParamsBtn.addEventListener('click', () => this.saveParameters());
- this.saveToFileBtn.addEventListener('click', () => this.saveParametersToFile());
- this.loadParamsBtn.addEventListener('click', () => this.loadParameters());
- this.resetParamsBtn.addEventListener('click', () => this.resetParameters());
- this.loadConfigFileBtn.addEventListener('click', () => this.loadConfigFile());
- this.configFileInput.addEventListener('change', (e) => this.handleConfigFileSelect(e));
- // 统一YMODEM相关按钮事件绑定
- const flmYmodemSendBtn = document.getElementById('flmYmodemSendBtn');
- if (flmYmodemSendBtn) {
- flmYmodemSendBtn.addEventListener('click', () => this.handleFlmYmodemSend());
- }
- // Python发送按钮事件绑定在setupPythonScriptPanel中处理
- }
- checkWebSerialSupport() {
- if (!('serial' in navigator)) {
- this.addMessage('错误: 您的浏览器不支持 Web Serial API。请使用 Chrome 89+ 或 Edge 89+', 'error');
- this.connectBtn.disabled = true;
- this.connectBtn.innerHTML = '<i class="fas fa-times"></i> 不支持';
- }
- }
- async connectSerial() {
- try {
- // 请求串口权限
- this.port = await navigator.serial.requestPort();
- // 获取串口配置
- const baudRate = this.baudRateSelect.value === 'custom'
- ? parseInt(this.customBaudRateInput.value)
- : parseInt(this.baudRateSelect.value);
- const dataBits = parseInt(this.dataBitsSelect.value);
- const stopBits = parseInt(this.stopBitsSelect.value);
- const parity = this.paritySelect.value;
- // 打开串口
- await this.port.open({
- baudRate: baudRate,
- dataBits: dataBits,
- stopBits: stopBits,
- parity: parity
- });
- this.isConnected = true;
- this.updateConnectionStatus(true);
- // 格式化校验位显示
- const parityDisplay = parity === 'none' ? 'N' : parity === 'even' ? 'E' : 'O';
- this.addMessage(`串口连接成功 - 波特率: ${baudRate}, 数据位: ${dataBits}${parityDisplay}${stopBits}`, 'info');
- // 开始读取数据
- this.startReading();
- } catch (error) {
- this.addMessage(`连接失败: ${error.message}`, 'error');
- }
- }
- async disconnectSerial() {
- try {
- // 先设置断开标志,停止读取循环
- this.isConnected = false;
- // 释放reader锁
- if (this.reader) {
- try {
- await this.reader.cancel();
- } catch (e) {
- // 忽略cancel错误,继续释放锁
- console.log('Reader cancel error:', e);
- }
- try {
- this.reader.releaseLock();
- } catch (e) {
- // 忽略releaseLock错误
- console.log('Reader releaseLock error:', e);
- }
- this.reader = null;
- }
- // 释放writer锁
- if (this.writer) {
- try {
- this.writer.releaseLock();
- } catch (e) {
- console.log('Writer releaseLock error:', e);
- }
- this.writer = null;
- }
- // 关闭串口
- if (this.port) {
- await this.port.close();
- this.port = null;
- }
- this.updateConnectionStatus(false);
- // 清理数据缓冲区
- this.flushBuffer();
- this.addMessage('串口已断开', 'info');
- } catch (error) {
- this.addMessage(`断开连接失败: ${error.message}`, 'error');
- // 强制重置状态
- this.isConnected = false;
- this.reader = null;
- this.writer = null;
- this.port = null;
- this.updateConnectionStatus(false);
- }
- }
- async startReading() {
- if (!this.port) return;
- try {
- this.reader = this.port.readable.getReader();
- while (this.isConnected) {
- const { value, done } = await this.reader.read();
- if (done) break;
- this.handleReceivedData(value);
- }
- } catch (error) {
- if (this.isConnected) {
- this.addMessage(`读取数据错误: ${error.message}`, 'error');
- }
- } finally {
- if (this.reader) {
- try {
- this.reader.releaseLock();
- } catch (e) {
- console.log('Reader releaseLock in finally:', e);
- }
- this.reader = null;
- }
- }
- }
- handleReceivedData(data) {
- // 记录数据接收时间
- const receiveTime = new Date();
- // 检查是否处于曲线绘制模式
- if (chartDrawEnabled && window.chartData) {
- // 曲线绘制模式:直接处理串口数据,不显示在主监控
- // console.log(`[曲线模式] 接收到 ${data.length} 字节数据:`, Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '));
-
- // 处理分片数据,确保4字节对齐
- handleRealtimeHexDataChunkedSimple(data);
-
- // 不显示在主监控,直接返回
- return;
- }
- if (this.hexModeCheckbox.checked) {
- // HEX 模式显示
- const hexText = Array.from(data)
- .map(byte => byte.toString(16).padStart(2, '0').toUpperCase())
- .join(' ');
- if (this.enableBufferCheckbox.checked) {
- // HEX模式也使用缓冲和超时机制
- this.bufferHexData(hexText);
- } else {
- // 直接显示,不缓冲
- this.addMessage(hexText, 'received', receiveTime);
- }
- } else {
- // 文本模式显示
- const text = new TextDecoder().decode(data);
- if (this.enableBufferCheckbox.checked) {
- // 使用缓冲和超时机制
- this.bufferData(text);
- } else {
- // 直接显示,不缓冲,使用接收时间作为时间戳
- this.addMessage(text, 'received', receiveTime);
- }
- }
- }
- bufferData(text) {
- // 调试模式:显示接收到的原始数据
- if (this.debugModeCheckbox.checked) {
- const debugInfo = `[DEBUG] 接收数据 (${text.length}字节): ${JSON.stringify(text)}`;
- this.addMessage(debugInfo, 'info');
- }
- // 记录缓冲区开始时间(只在缓冲区为空时记录)
- if (this.dataBuffer.length === 0) {
- this.bufferStartTime = new Date();
- }
- // 将新数据添加到缓冲区
- this.dataBuffer += text;
- // 清除之前的超时(这是关键!每次新数据都重置超时)
- if (this.lineTimeoutId) {
- clearTimeout(this.lineTimeoutId);
- this.lineTimeoutId = null;
- if (this.debugModeCheckbox.checked) {
- this.addMessage('[DEBUG] 重置超时计时器', 'info');
- }
- }
- // 检查是否包含明确的结束标志
- const hasLineEnding = this.dataBuffer.includes('\n') || this.dataBuffer.includes('\r');
- // 检查是否是单独的shell提示符(应该保持在同一行)
- const isStandalonePrompt = this.dataBuffer.match(/^sh:\/\$\s*$/) ||
- this.dataBuffer.match(/^>>>\s*$/) ||
- this.dataBuffer.match(/^>\s*$/);
- // 检查是否包含完整的命令输出(以换行符+提示符结尾)
- const hasCompleteOutput = this.dataBuffer.match(/\n\s*sh:\/\$\s*$/) ||
- this.dataBuffer.match(/\r\n\s*sh:\/\$\s*$/) ||
- this.dataBuffer.match(/\n\s*>>>\s*$/) ||
- this.dataBuffer.match(/\r\n\s*>>>\s*$/);
- // 检查是否包含完整的命令列表结束
- const hasCommandListEnd = this.dataBuffer.includes('Command List:') &&
- (this.dataBuffer.includes('\nsh:/$ ') || this.dataBuffer.includes('\r\nsh:/$ '));
- // 检查缓冲区大小,如果太大就强制刷新
- const bufferTooLarge = this.dataBuffer.length > 2000;
- // 立即刷新的条件
- if (hasCompleteOutput || hasCommandListEnd || bufferTooLarge) {
- // 立即刷新,不等待超时
- this.flushBuffer();
- return;
- }
- // 如果是单独的提示符,不立即刷新,等待后续内容
- if (isStandalonePrompt) {
- // 使用较长的超时等待后续内容
- const timeout = parseInt(this.lineTimeoutInput.value) || 50;
- this.lineTimeoutId = setTimeout(() => {
- this.flushBuffer();
- }, timeout * 2); // 使用双倍超时时间
- return;
- }
- // 如果包含换行符,检查是否是完整的行
- if (hasLineEnding) {
- // 检查是否以换行符结尾(完整的行)
- const endsWithNewline = this.dataBuffer.endsWith('\n') || this.dataBuffer.endsWith('\r\n') || this.dataBuffer.endsWith('\r');
- if (endsWithNewline) {
- // 完整的行,使用较短的超时
- if (this.debugModeCheckbox.checked) {
- this.addMessage(`[DEBUG] 检测到完整行,使用短超时(10ms)`, 'info');
- }
- this.lineTimeoutId = setTimeout(() => {
- this.flushBuffer();
- }, 10);
- } else {
- // 包含换行符但不以换行符结尾,可能还有更多数据,使用配置的超时
- const timeout = parseInt(this.lineTimeoutInput.value) || 50;
- if (this.debugModeCheckbox.checked) {
- this.addMessage(`[DEBUG] 包含换行符但不完整,使用配置超时(${timeout}ms)`, 'info');
- }
- this.lineTimeoutId = setTimeout(() => {
- this.flushBuffer();
- }, timeout);
- }
- return;
- }
- // 默认情况下,设置超时等待更多数据
- const baseTimeout = parseInt(this.lineTimeoutInput.value) || 50;
- // 根据数据特征调整超时时间
- let timeout = baseTimeout;
- // 如果包含ANSI序列,使用更短的超时
- if (this.dataBuffer.includes('\x1b[')) {
- timeout = Math.min(baseTimeout, 20);
- if (this.debugModeCheckbox.checked) {
- this.addMessage(`[DEBUG] 检测到ANSI序列,调整超时为${timeout}ms`, 'info');
- }
- }
- // 如果数据看起来像是命令输出的一部分,使用更短的超时
- if (this.dataBuffer.includes('CMD') || this.dataBuffer.includes('--------')) {
- timeout = Math.min(baseTimeout, 15);
- if (this.debugModeCheckbox.checked) {
- this.addMessage(`[DEBUG] 检测到命令输出,调整超时为${timeout}ms`, 'info');
- }
- }
- if (this.debugModeCheckbox.checked) {
- this.addMessage(`[DEBUG] 设置默认超时: ${timeout}ms (基础: ${baseTimeout}ms)`, 'info');
- }
- this.lineTimeoutId = setTimeout(() => {
- this.flushBuffer();
- }, timeout);
- }
- bufferHexData(hexText) {
- // 调试模式:显示接收到的原始HEX数据
- if (this.debugModeCheckbox.checked) {
- const debugInfo = `[DEBUG] 接收HEX数据: ${hexText}`;
- this.addMessage(debugInfo, 'info');
- }
- // 记录缓冲区开始时间(只在缓冲区为空时记录)
- if (this.dataBuffer.length === 0) {
- this.bufferStartTime = new Date();
- }
- // 将新的HEX数据添加到缓冲区(用空格分隔)
- if (this.dataBuffer.length > 0) {
- this.dataBuffer += ' ' + hexText;
- } else {
- this.dataBuffer = hexText;
- }
- // 清除之前的超时(每次新数据都重置超时)
- if (this.lineTimeoutId) {
- clearTimeout(this.lineTimeoutId);
- this.lineTimeoutId = null;
- if (this.debugModeCheckbox.checked) {
- this.addMessage('[DEBUG] 重置HEX超时计时器', 'info');
- }
- }
- // HEX模式下的缓冲逻辑相对简单,主要基于超时
- const timeout = parseInt(this.lineTimeoutInput.value) || 50;
- // 检查缓冲区大小,如果太大就强制刷新
- const bufferTooLarge = this.dataBuffer.length > 1000; // HEX数据较长,适当增加限制
- if (bufferTooLarge) {
- if (this.debugModeCheckbox.checked) {
- this.addMessage('[DEBUG] HEX缓冲区过大,强制刷新', 'info');
- }
- this.flushBuffer();
- return;
- }
- if (this.debugModeCheckbox.checked) {
- this.addMessage(`[DEBUG] 设置HEX超时: ${timeout}ms`, 'info');
- }
- this.lineTimeoutId = setTimeout(() => {
- this.flushBuffer();
- }, timeout);
- }
- flushBuffer() {
- if (this.dataBuffer.length > 0) {
- if (this.debugModeCheckbox.checked) {
- this.addMessage(`[DEBUG] 刷新缓冲区 (${this.dataBuffer.length}字节)`, 'info');
- }
- // 根据配置决定是否处理ANSI序列
- let processedData = this.processAnsiSequencesCheckbox.checked ?
- this.processAnsiSequences(this.dataBuffer) :
- this.dataBuffer;
- // 显示处理后的数据,使用缓冲区开始时间作为时间戳
- this.addMessage(processedData, 'received', this.bufferStartTime);
- // 清空缓冲区和时间戳
- this.dataBuffer = '';
- this.bufferStartTime = null;
- }
- // 清除超时
- if (this.lineTimeoutId) {
- clearTimeout(this.lineTimeoutId);
- this.lineTimeoutId = null;
- }
- }
- processAnsiSequences(text) {
- // 处理ANSI转义序列,使其在HTML中正确显示
- let processed = text;
- // 首先处理特殊的控制序列
- // 处理 [2K (清除整行)
- processed = processed.replace(/\x1b\[2K/g, '');
- // 处理光标移动序列
- processed = processed.replace(/\x1b\[[0-9]+;[0-9]+H/g, '');
- processed = processed.replace(/\x1b\[[0-9]+;[0-9]+f/g, '');
- // 处理回车符覆盖行为(这是关键!)
- // 当遇到 \r 但没有 \n 时,表示要覆盖当前行
- processed = this.processCarriageReturn(processed);
- // 处理ANSI颜色和格式代码
- processed = this.processAnsiColors(processed);
- // 移除其他未处理的ANSI转义序列
- processed = processed.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
- // 移除一些控制字符,但保留换行符和制表符
- processed = processed.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
- // 清理多余的空行(连续的换行符)
- processed = processed.replace(/\n{3,}/g, '\n\n');
- return processed;
- }
- processCarriageReturn(text) {
- // 处理回车符的覆盖行为
- let processed = text;
- // 检查是否包含需要处理的覆盖模式
- if (processed.includes('\r') && !processed.includes('\r\n')) {
- // 只处理特定的shell提示符覆盖模式
- // 模式1: 调试信息被提示符覆盖 (如: "2025-05-30 11:21:01 DEBUG: MQTT...\rsh:/$ ")
- processed = processed.replace(/(.*DEBUG[^\r]*)\r(sh:\/\$\s*)/g, (match, content, prompt) => {
- // 如果调试信息很长,保留它并添加换行,然后显示提示符
- if (content.length > 10) {
- return content + '\n' + prompt;
- } else {
- return prompt; // 短内容直接被覆盖
- }
- });
- // 模式2: 其他内容被提示符覆盖,但保留有意义的内容
- processed = processed.replace(/([^\r\n]{10,})\r(sh:\/\$\s*|>>>\s*|>\s*)$/gm, '$1\n$2');
- // 模式3: 短内容被提示符覆盖(真正的覆盖行为)
- processed = processed.replace(/([^\r\n]{1,9})\r(sh:\/\$\s*|>>>\s*|>\s*)$/gm, '$2');
- }
- return processed;
- }
- processAnsiColors(text) {
- let processed = text;
- // 定义ANSI颜色映射(使用终端友好的颜色)
- const ansiColors = {
- // 重置
- '0': { action: 'reset' },
- // 文本样式
- '1': { style: 'font-weight: bold;' }, // 粗体
- '2': { style: 'opacity: 0.6;' }, // 暗淡
- '3': { style: 'font-style: italic;' }, // 斜体
- '4': { style: 'text-decoration: underline;' }, // 下划线
- '5': { style: 'animation: blink 1s infinite;' }, // 闪烁
- '7': { style: 'filter: invert(1);' }, // 反色
- '9': { style: 'text-decoration: line-through;' }, // 删除线
- // 前景色(标准颜色)
- '30': { color: '#2e3436' }, // 黑色
- '31': { color: '#cc0000' }, // 红色
- '32': { color: '#4e9a06' }, // 绿色
- '33': { color: '#c4a000' }, // 黄色
- '34': { color: '#3465a4' }, // 蓝色
- '35': { color: '#75507b' }, // 洋红
- '36': { color: '#06989a' }, // 青色
- '37': { color: '#d3d7cf' }, // 白色
- // 前景色(高亮颜色)
- '90': { color: '#555753' }, // 亮黑色(灰色)
- '91': { color: '#ef2929' }, // 亮红色
- '92': { color: '#8ae234' }, // 亮绿色
- '93': { color: '#fce94f' }, // 亮黄色
- '94': { color: '#729fcf' }, // 亮蓝色
- '95': { color: '#ad7fa8' }, // 亮洋红
- '96': { color: '#34e2e2' }, // 亮青色
- '97': { color: '#eeeeec' }, // 亮白色
- // 背景色(标准颜色)
- '40': { backgroundColor: '#2e3436' }, // 黑色背景
- '41': { backgroundColor: '#cc0000' }, // 红色背景
- '42': { backgroundColor: '#4e9a06' }, // 绿色背景
- '43': { backgroundColor: '#c4a000' }, // 黄色背景
- '44': { backgroundColor: '#3465a4' }, // 蓝色背景
- '45': { backgroundColor: '#75507b' }, // 洋红背景
- '46': { backgroundColor: '#06989a' }, // 青色背景
- '47': { backgroundColor: '#d3d7cf' }, // 白色背景
- // 背景色(高亮颜色)
- '100': { backgroundColor: '#555753' }, // 亮黑色背景
- '101': { backgroundColor: '#ef2929' }, // 亮红色背景
- '102': { backgroundColor: '#8ae234' }, // 亮绿色背景
- '103': { backgroundColor: '#fce94f' }, // 亮黄色背景
- '104': { backgroundColor: '#729fcf' }, // 亮蓝色背景
- '105': { backgroundColor: '#ad7fa8' }, // 亮洋红背景
- '106': { backgroundColor: '#34e2e2' }, // 亮青色背景
- '107': { backgroundColor: '#eeeeec' }, // 亮白色背景
- };
- // 处理ANSI转义序列
- processed = processed.replace(/\x1b\[([0-9;]*)m/g, (match, codes) => {
- if (!codes) codes = '0'; // 空代码默认为重置
- const codeList = codes.split(';');
- let styles = [];
- let hasReset = false;
- let i = 0;
- while (i < codeList.length) {
- const code = codeList[i];
- // 处理256色和RGB颜色
- if (code === '38' || code === '48') { // 前景色或背景色
- const isBackground = code === '48';
- i++;
- if (i < codeList.length && codeList[i] === '5') {
- // 256色模式: ESC[38;5;n m 或 ESC[48;5;n m
- i++;
- if (i < codeList.length) {
- const colorIndex = parseInt(codeList[i]);
- const color = this.get256Color(colorIndex);
- if (isBackground) {
- styles.push(`background-color: ${color}`);
- } else {
- styles.push(`color: ${color}`);
- }
- }
- } else if (i < codeList.length && codeList[i] === '2') {
- // RGB模式: ESC[38;2;r;g;b m 或 ESC[48;2;r;g;b m
- i++;
- if (i + 2 < codeList.length) {
- const r = parseInt(codeList[i]);
- const g = parseInt(codeList[i + 1]);
- const b = parseInt(codeList[i + 2]);
- const color = `rgb(${r}, ${g}, ${b})`;
- if (isBackground) {
- styles.push(`background-color: ${color}`);
- } else {
- styles.push(`color: ${color}`);
- }
- i += 2;
- }
- }
- } else {
- // 处理标准ANSI代码
- const ansiCode = ansiColors[code];
- if (ansiCode) {
- if (ansiCode.action === 'reset') {
- hasReset = true;
- break;
- } else {
- if (ansiCode.color) styles.push(`color: ${ansiCode.color}`);
- if (ansiCode.backgroundColor) styles.push(`background-color: ${ansiCode.backgroundColor}`);
- if (ansiCode.style) styles.push(ansiCode.style);
- }
- }
- }
- i++;
- }
- if (hasReset) {
- return '</span>';
- } else if (styles.length > 0) {
- return `<span style="${styles.join('; ')};">`;
- } else {
- return ''; // 未知代码,移除
- }
- });
- return processed;
- }
- get256Color(index) {
- // 256色调色板
- if (index < 16) {
- // 标准16色
- const colors = [
- '#000000', '#800000', '#008000', '#808000', '#000080', '#800080', '#008080', '#c0c0c0',
- '#808080', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff'
- ];
- return colors[index];
- } else if (index < 232) {
- // 216色立方体 (6x6x6)
- const i = index - 16;
- const r = Math.floor(i / 36);
- const g = Math.floor((i % 36) / 6);
- const b = i % 6;
- const toHex = (n) => {
- const values = [0, 95, 135, 175, 215, 255];
- return values[n].toString(16).padStart(2, '0');
- };
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
- } else {
- // 24级灰度
- const gray = 8 + (index - 232) * 10;
- const hex = gray.toString(16).padStart(2, '0');
- return `#${hex}${hex}${hex}`;
- }
- }
- escapeRegExp(string) {
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- }
- // MicroLink API 方法
- async startRTT() {
- const addr = this.rttAddrInput.value;
- const size = this.rttSizeInput.value;
- const channel = this.rttChannelInput.value;
- const command = `RTTView.start(${addr}, ${size}, ${channel})`;
- await this.sendCommand(command);
- }
- async stopRTT() {
- const command = 'RTTView.stop()';
- await this.sendCommand(command);
- }
- async startSystemView() {
- const addr = this.svAddrInput.value;
- const size = this.svSizeInput.value;
- const channel = this.svChannelInput.value;
- const command = `SystemView.start(${addr}, ${size}, ${channel})`;
- await this.sendCommand(command);
- }
- async loadFLM() {
- const flmPath = this.flmPathInput.value;
- const baseAddr = this.baseAddrInput.value;
- const ramAddr = this.ramAddrInput.value;
- const command = `ReadFlm.load("${flmPath}", ${baseAddr}, ${ramAddr})`;
- await this.sendCommand(command);
- }
- async loadBin() {
- const binPath = this.binPathInput.value;
- const binAddr = this.binAddrInput.value;
- const command = `load.bin("${binPath}", ${binAddr})`;
- await this.sendCommand(command);
- }
- async offlineDownload() {
- const command = 'load.offline()';
- await this.sendCommand(command);
- }
- async ymodemSend() {
- const filePath = this.ymodemFileInput.value;
- const command = `ym.send("${filePath}")`;
- await this.sendCommand(command);
- }
- async sendCustomCommand() {
- const command = this.customCommandInput.value.trim();
- if (command) {
- if (this.hexModeCheckbox.checked) {
- // HEX模式:发送十六进制数据
- await this.sendHexData(command);
- } else {
- // 文本模式:发送普通命令
- await this.sendCommand(command);
- }
- }
- }
- async sendCommand(command) {
- if (!this.isConnected) {
- this.addMessage('请先连接串口', 'error');
- return;
- }
- try {
- const dataToSend = new TextEncoder().encode(command + '\r\n');
- if (!this.writer) {
- this.writer = this.port.writable.getWriter();
- }
- await this.writer.write(dataToSend);
- this.addMessage(`命令: ${command}`, 'sent');
- } catch (error) {
- this.addMessage(`发送命令失败: ${error.message}`, 'error');
- }
- }
- addCustomCommand() {
- const command = this.customCommandInput.value.trim();
- if (command && !this.customCommands.includes(command)) {
- this.customCommands.push(command);
- this.updateCustomCommandsList();
- this.customCommandInput.value = '';
- }
- }
- updateCustomCommandsList() {
- this.customCommandsList.innerHTML = '';
- this.customCommands.forEach((command, index) => {
- const item = document.createElement('div');
- item.className = 'custom-command-item';
- item.textContent = command;
- item.addEventListener('click', () => {
- this.customCommandInput.value = command;
- });
- this.customCommandsList.appendChild(item);
- });
- }
- addMessage(text, type = 'info', customTimestamp = null) {
- const message = document.createElement('div');
- message.className = `message message-${type}`;
- // 使用自定义时间戳或当前时间
- const messageTime = customTimestamp || new Date();
- let content = '';
- if (this.showTimestampCheckbox.checked) {
- const hours = messageTime.getHours().toString().padStart(2, '0');
- const minutes = messageTime.getMinutes().toString().padStart(2, '0');
- const seconds = messageTime.getSeconds().toString().padStart(2, '0');
- const milliseconds = messageTime.getMilliseconds().toString().padStart(3, '0');
- const timestamp = `${hours}:${minutes}:${seconds}.<span class="milliseconds">${milliseconds}</span>`;
- content += `<span class="timestamp">[${timestamp}]</span>`;
- }
- content += text;
- message.innerHTML = content;
- // 保存到日志记录中,使用消息的实际时间戳
- this.logMessages.push({
- timestamp: messageTime,
- type: type,
- content: text
- });
- this.terminalOutput.appendChild(message);
- if (this.autoScrollCheckbox.checked) {
- this.scrollToBottom();
- }
- }
- scrollToBottom() {
- this.terminalOutput.scrollTop = this.terminalOutput.scrollHeight;
- }
- updateTerminalPlaceholder() {
- if (this.isConnected) {
- if (this.hexModeCheckbox.checked) {
- this.terminalInputField.placeholder = 'HEX模式:输入十六进制数据(如:41 42 43 或 414243)后按Enter发送...';
- } else {
- this.terminalInputField.placeholder = '输入命令后点击发送或按Enter键发送...';
- }
- } else {
- this.terminalInputField.placeholder = '请先连接串口...';
- }
- }
- updateSendOptions() {
- const isHexMode = this.hexModeCheckbox.checked;
- if (isHexMode) {
- // HEX模式下:保存当前设置并禁用虚拟终端模式
- this.virtualTerminalModeOriginalValue = this.virtualTerminalModeCheckbox.checked;
- this.virtualTerminalModeCheckbox.disabled = true;
- this.virtualTerminalMode = false; // 功能上禁用,但保持复选框原始状态
- // 添加视觉提示
- const label = this.virtualTerminalModeCheckbox.parentElement;
- label.style.opacity = '0.5';
- label.title = 'HEX模式下虚拟终端功能不可用';
- } else {
- // 文本模式下:恢复虚拟终端模式
- this.virtualTerminalModeCheckbox.disabled = false;
- this.virtualTerminalModeCheckbox.checked = this.virtualTerminalModeOriginalValue;
- this.virtualTerminalMode = this.virtualTerminalModeOriginalValue;
- // 移除视觉提示
- const label = this.virtualTerminalModeCheckbox.parentElement;
- label.style.opacity = '1';
- label.title = '';
- }
- // 更新终端占位符
- this.updateTerminalPlaceholder();
- }
- clearTerminal() {
- if (confirm('确定要清空终端内容吗?')) {
- this.terminalOutput.innerHTML = '';
- this.logMessages = [];
- this.addMessage('终端已清空', 'info');
- }
- }
- updateConnectionStatus(connected) {
- if (connected) {
- this.connectionStatus.className = 'status-connected';
- this.connectionStatus.innerHTML = '<i class="fas fa-circle"></i> 已连接';
- this.connectBtn.disabled = true;
- this.disconnectBtn.disabled = false;
- this.terminalInputField.disabled = false;
- if (this.terminalSendBtn) this.terminalSendBtn.disabled = false;
- this.updateTerminalPlaceholder();
- } else {
- this.connectionStatus.className = 'status-disconnected';
- this.connectionStatus.innerHTML = '<i class="fas fa-circle"></i> 未连接';
- this.connectBtn.disabled = false;
- this.disconnectBtn.disabled = true;
- this.terminalInputField.disabled = true;
- if (this.terminalSendBtn) this.terminalSendBtn.disabled = true;
- this.updateTerminalPlaceholder();
- }
- }
- // 参数管理方法
- saveParameters() {
- const params = {
- baudRate: this.baudRateSelect.value,
- customBaudRate: this.customBaudRateInput.value,
- dataBits: this.dataBitsSelect.value,
- parity: this.paritySelect.value,
- stopBits: this.stopBitsSelect.value,
- rttAddr: this.rttAddrInput.value,
- rttSize: this.rttSizeInput.value,
- rttChannel: this.rttChannelInput.value,
- svAddr: this.svAddrInput.value,
- svSize: this.svSizeInput.value,
- svChannel: this.svChannelInput.value,
- flmPath: this.flmPathInput.value,
- baseAddr: this.baseAddrInput.value,
- ramAddr: this.ramAddrInput.value,
- binPath: this.binPathInput.value,
- binAddr: this.binAddrInput.value,
- ymodemFile: this.ymodemFileInput.value,
- customCommands: this.customCommands,
- virtualTerminalMode: this.virtualTerminalModeOriginalValue,
- processAnsiSequences: this.processAnsiSequencesCheckbox.checked
- };
- localStorage.setItem('microlinkParams', JSON.stringify(params));
- this.addMessage('✅ 参数已保存到本地存储', 'info');
- }
- async saveParametersToFile() {
- try {
- // 收集当前参数
- const params = this.getCurrentParameters();
- // 生成配置文件内容
- const configContent = this.generateConfigFileContent(params);
- // 检查是否支持File System Access API
- if ('showSaveFilePicker' in window) {
- await this.saveWithFilePicker(configContent);
- } else {
- // 降级到传统下载方式
- this.saveWithDownload(configContent);
- }
- } catch (error) {
- if (error.name === 'AbortError') {
- this.addMessage('💡 用户取消了文件保存', 'info');
- } else {
- this.addMessage(`❌ 保存参数到文件失败: ${error.message}`, 'error');
- }
- }
- }
- async saveWithFilePicker(configContent) {
- try {
- // 使用File System Access API让用户选择保存位置
- const fileHandle = await window.showSaveFilePicker({
- suggestedName: 'microlink_web_scp.txt',
- types: [
- {
- description: 'MicroLink配置文件',
- accept: {
- 'text/plain': ['.txt'],
- },
- },
- ],
- });
- // 创建可写流并写入内容
- const writable = await fileHandle.createWritable();
- await writable.write(configContent);
- await writable.close();
- this.addMessage('✅ 参数已保存到指定位置', 'info');
- } catch (error) {
- if (error.name === 'AbortError') {
- throw error; // 重新抛出取消错误
- } else {
- // 如果File System Access API失败,降级到下载方式
- this.addMessage('⚠️ 文件选择器不可用,使用下载方式保存', 'info');
- this.saveWithDownload(configContent);
- }
- }
- }
- saveWithDownload(configContent) {
- // 传统的下载方式
- const blob = new Blob([configContent], { type: 'text/plain;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- // 创建下载链接并触发下载
- const a = document.createElement('a');
- a.href = url;
- a.download = 'microlink_web_scp.txt';
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- // 清理URL对象
- URL.revokeObjectURL(url);
- this.addMessage('✅ 参数已保存到下载文件夹 microlink_web_scp.txt', 'info');
- }
- getCurrentParameters() {
- return {
- baudRate: this.baudRateSelect.value,
- customBaudRate: this.customBaudRateInput.value,
- dataBits: this.dataBitsSelect.value,
- parity: this.paritySelect.value,
- stopBits: this.stopBitsSelect.value,
- rttAddr: this.rttAddrInput.value,
- rttSize: this.rttSizeInput.value,
- rttChannel: this.rttChannelInput.value,
- svAddr: this.svAddrInput.value,
- svSize: this.svSizeInput.value,
- svChannel: this.svChannelInput.value,
- flmPath: this.flmPathInput.value,
- baseAddr: this.baseAddrInput.value,
- ramAddr: this.ramAddrInput.value,
- binPath: this.binPathInput.value,
- binAddr: this.binAddrInput.value,
- ymodemFile: this.ymodemFileInput.value,
- customCommands: this.customCommands,
- virtualTerminalMode: this.virtualTerminalModeOriginalValue,
- processAnsiSequences: this.processAnsiSequencesCheckbox.checked
- };
- }
- generateConfigFileContent(params) {
- // 生成配置文件格式的内容
- const lines = [
- '# MicroLink Web Serial Configuration Parameters',
- '# 串口配置',
- 'port=COM3',
- `baudrate=${params.baudRate}`,
- `databits=${params.dataBits}`,
- `parity=${params.parity === 'none' ? 'N' : params.parity === 'even' ? 'E' : 'O'}`,
- `stopbits=${params.stopBits}`,
- '',
- '# RTT配置',
- `rtt_addr=${params.rttAddr}`,
- `rtt_size=${params.rttSize}`,
- `rtt_channel=${params.rttChannel}`,
- '',
- '# SystemView配置',
- `systemview_addr=${params.svAddr}`,
- `systemview_size=${params.svSize}`,
- `systemview_channel=${params.svChannel}`,
- '',
- '# FLM配置',
- `flm_path=${params.flmPath}`,
- `base_addr=${params.baseAddr}`,
- `ram_addr=${params.ramAddr}`,
- '',
- '# 下载配置',
- `bin_file_path=${params.binPath}`,
- `bin_addr=${params.binAddr}`,
- '',
- '# 自定义命令',
- `custom_commands=${params.customCommands.join(';')}`,
- ''
- ];
- return lines.join('\n');
- }
- async loadParameters() {
- // 直接从HTML配置加载
- this.loadConfigFromHTML();
- // 然后检查是否有用户保存的参数覆盖
- const saved = localStorage.getItem('microlinkParams');
- if (saved) {
- try {
- const params = JSON.parse(saved);
- this.applyParameters(params);
- this.addMessage('✅ 用户保存的参数已加载并覆盖HTML配置', 'info');
- } catch (error) {
- this.addMessage('❌ 用户参数格式错误,使用HTML配置参数', 'error');
- }
- }
- }
- async loadConfigFile() {
- // 手动重新加载HTML配置(按钮触发)
- this.addMessage('🔄 手动重新加载HTML配置...', 'info');
-
- try {
- const configElement = document.getElementById('embedded-config');
- if (configElement) {
- const configText = configElement.textContent;
- const params = this.parseConfigFile(configText);
- this.applyParameters(params);
- this.addMessage('✅ 参数已从HTML配置重新加载', 'info');
- return;
- } else {
- this.addMessage('❌ HTML配置元素未找到', 'error');
- }
- } catch (error) {
- this.addMessage(`从HTML配置加载失败: ${error.message}`, 'error');
- }
- // 如果HTML配置失败,使用备用配置
- this.addMessage('⚠️ HTML配置加载失败,使用备用配置', 'info');
- this.loadEmbeddedConfig();
- }
- loadConfigFromHTML() {
- try {
- // 从HTML中的script标签读取配置
- const configElement = document.getElementById('embedded-config');
- if (configElement) {
- const configText = configElement.textContent;
- const params = this.parseConfigFile(configText);
- this.applyParameters(params);
- this.addMessage('✅ 参数已从HTML配置加载', 'info');
- } else {
- // 如果HTML中没有配置,使用硬编码的备用配置
- this.loadEmbeddedConfig();
- }
- } catch (error) {
- this.addMessage(`从HTML配置加载失败: ${error.message}`, 'error');
- this.loadEmbeddedConfig();
- }
- }
- loadEmbeddedConfig() {
- // 备用的硬编码配置
- const embeddedConfig = `# MicroLink Web Serial Configuration Parameters
- # 串口配置
- port=COM3
- baudrate=115200
- databits=8
- parity=N
- stopbits=1
- # RTT配置
- rtt_addr=0x20000000
- rtt_size=0x4000
- rtt_channel=0
- # SystemView配置
- systemview_addr=0x20000000
- systemview_size=0x4000
- systemview_channel=1
- # FLM配置
- flm_path=STM32/STM32F4xx_1024.FLM.o
- base_addr=0x08000000
- ram_addr=0x20000000
- # 下载配置
- bin_file_path=firmware.bin
- bin_addr=0x08000000
- # 自定义命令
- custom_commands=RTTView.start(0x20000000,1024,0);SystemView.start(0x20000000,1024,1);load.offline()`;
- const params = this.parseConfigFile(embeddedConfig);
- this.applyParameters(params);
- this.addMessage('参数已从备用配置加载', 'info');
- }
- handleConfigFileSelect(event) {
- const file = event.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = (e) => {
- try {
- const configText = e.target.result;
- const params = this.parseConfigFile(configText);
- this.applyParameters(params);
- this.addMessage(`参数已从文件 "${file.name}" 加载`, 'info');
- } catch (error) {
- this.addMessage(`解析配置文件失败: ${error.message}`, 'error');
- }
- };
- reader.readAsText(file);
- }
- }
- parseConfigFile(configText) {
- const params = {};
- const lines = configText.split('\n');
- for (const line of lines) {
- const trimmedLine = line.trim();
- if (trimmedLine && !trimmedLine.startsWith('#')) {
- const [key, value] = trimmedLine.split('=');
- if (key && value) {
- const trimmedKey = key.trim();
- const trimmedValue = value.trim();
- // 映射配置文件的键到内部参数名
- switch (trimmedKey) {
- case 'baudrate':
- params.baudRate = trimmedValue;
- break;
- case 'databits':
- params.dataBits = trimmedValue;
- break;
- case 'parity':
- params.parity = trimmedValue.toLowerCase() === 'n' ? 'none' :
- trimmedValue.toLowerCase() === 'e' ? 'even' : 'odd';
- break;
- case 'stopbits':
- params.stopBits = trimmedValue;
- break;
- case 'rtt_addr':
- params.rttAddr = trimmedValue;
- break;
- case 'rtt_size':
- params.rttSize = trimmedValue;
- break;
- case 'rtt_channel':
- params.rttChannel = trimmedValue;
- break;
- case 'systemview_addr':
- params.svAddr = trimmedValue;
- break;
- case 'systemview_size':
- params.svSize = trimmedValue;
- break;
- case 'systemview_channel':
- params.svChannel = trimmedValue;
- break;
- case 'flm_path':
- params.flmPath = trimmedValue;
- break;
- case 'base_addr':
- params.baseAddr = trimmedValue;
- break;
- case 'ram_addr':
- params.ramAddr = trimmedValue;
- break;
- case 'bin_file_path':
- params.binPath = trimmedValue;
- break;
- case 'bin_addr':
- params.binAddr = trimmedValue;
- break;
- case 'custom_commands':
- params.customCommands = trimmedValue.split(';').filter(cmd => cmd.trim());
- break;
- }
- }
- }
- }
- return params;
- }
- resetParameters() {
- // 重置为默认值(与HTML配置保持一致)
- const defaultParams = {
- baudRate: '115200',
- customBaudRate: '',
- dataBits: '8',
- parity: 'none',
- stopBits: '1',
- rttAddr: '0x20000000',
- rttSize: '0x4000',
- rttChannel: '0',
- svAddr: '0x20000000',
- svSize: '0x4000',
- svChannel: '1',
- flmPath: 'STM32/STM32F4xx_1024.FLM.o',
- baseAddr: '0x08000000',
- ramAddr: '0x20000000',
- binPath: 'firmware.bin',
- binAddr: '0x08000000',
- ymodemFile: 'update.bin',
- customCommands: [],
- virtualTerminalMode: true,
- processAnsiSequences: true
- };
- this.applyParameters(defaultParams);
- this.addMessage('参数已重置为默认值', 'info');
- }
- applyParameters(params) {
- this.baudRateSelect.value = params.baudRate || '115200';
- this.customBaudRateInput.value = params.customBaudRate || '';
- this.dataBitsSelect.value = params.dataBits || '8';
- this.paritySelect.value = params.parity || 'none';
- this.stopBitsSelect.value = params.stopBits || '1';
- this.rttAddrInput.value = params.rttAddr || '0x20000000';
- this.rttSizeInput.value = params.rttSize || '0x4000';
- this.rttChannelInput.value = params.rttChannel || '0';
- this.svAddrInput.value = params.svAddr || '0x20000000';
- this.svSizeInput.value = params.svSize || '0x4000';
- this.svChannelInput.value = params.svChannel || '1';
- this.flmPathInput.value = params.flmPath || 'STM32/STM32F4xx_1024.FLM.o';
- this.baseAddrInput.value = params.baseAddr || '0x08000000';
- this.ramAddrInput.value = params.ramAddr || '0x20000000';
- this.binPathInput.value = params.binPath || 'firmware.bin';
- this.binAddrInput.value = params.binAddr || '0x08000000';
- this.ymodemFileInput.value = params.ymodemFile || 'update.bin';
- this.customCommands = params.customCommands || [];
- this.updateCustomCommandsList();
- // 应用虚拟终端配置
- if (params.virtualTerminalMode !== undefined) {
- this.virtualTerminalModeCheckbox.checked = params.virtualTerminalMode;
- this.virtualTerminalMode = params.virtualTerminalMode;
- this.virtualTerminalModeOriginalValue = params.virtualTerminalMode;
- }
- if (params.processAnsiSequences !== undefined) {
- this.processAnsiSequencesCheckbox.checked = params.processAnsiSequences;
- }
- // 处理自定义波特率显示
- if (this.baudRateSelect.value === 'custom') {
- this.customBaudRateInput.style.display = 'block';
- } else {
- this.customBaudRateInput.style.display = 'none';
- }
- }
- // 终端输入处理方法
- handleTerminalInput(event) {
- if (!this.isConnected) {
- return; // 未连接时不处理按键
- }
- // 现在统一使用传统模式,不再实时发送
- this.handleLocalTerminalKey(event);
- }
- // 虚拟终端按键处理
- async handleVirtualTerminalKey(event) {
- event.preventDefault();
- let keySequence = null;
- let shouldClearInput = false;
- switch (event.key) {
- case 'Enter':
- keySequence = '\r\n';
- this.currentLine = '';
- shouldClearInput = true;
- break;
- case 'Tab':
- keySequence = '\t';
- shouldClearInput = true;
- break;
- case 'ArrowUp':
- keySequence = '\x1b[A'; // ANSI escape sequence for up arrow
- shouldClearInput = true;
- break;
- case 'ArrowDown':
- keySequence = '\x1b[B'; // ANSI escape sequence for down arrow
- shouldClearInput = true;
- break;
- case 'ArrowLeft':
- keySequence = '\x1b[D'; // ANSI escape sequence for left arrow
- shouldClearInput = true;
- break;
- case 'ArrowRight':
- keySequence = '\x1b[C'; // ANSI escape sequence for right arrow
- shouldClearInput = true;
- break;
- case 'Backspace':
- keySequence = '\x08'; // Backspace character
- if (this.currentLine.length > 0) {
- this.currentLine = this.currentLine.slice(0, -1);
- }
- shouldClearInput = true;
- break;
- case 'Delete':
- keySequence = '\x7f'; // DEL character
- shouldClearInput = true;
- break;
- case 'Home':
- keySequence = '\x1b[H';
- shouldClearInput = true;
- break;
- case 'End':
- keySequence = '\x1b[F';
- shouldClearInput = true;
- break;
- case 'PageUp':
- keySequence = '\x1b[5~';
- shouldClearInput = true;
- break;
- case 'PageDown':
- keySequence = '\x1b[6~';
- shouldClearInput = true;
- break;
- case 'Escape':
- keySequence = '\x1b';
- shouldClearInput = true;
- break;
- default:
- // 普通字符
- if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
- keySequence = event.key;
- this.currentLine += event.key;
- shouldClearInput = true;
- }
- // Ctrl组合键
- else if (event.ctrlKey && event.key.length === 1) {
- const char = event.key.toLowerCase();
- if (char >= 'a' && char <= 'z') {
- keySequence = String.fromCharCode(char.charCodeAt(0) - 96); // Ctrl+A = 0x01, etc.
- shouldClearInput = true;
- }
- }
- break;
- }
- if (keySequence) {
- // 发送按键序列到串口
- await this.sendRawData(keySequence);
- // 在虚拟终端模式下,清除输入框内容,让远程设备控制显示
- if (shouldClearInput) {
- this.terminalInputField.value = '';
- this.currentLine = '';
- }
- }
- }
- // 传统终端按键处理
- handleLocalTerminalKey(event) {
- switch (event.key) {
- case 'Enter':
- event.preventDefault();
- this.executeTerminalCommand();
- break;
- case 'ArrowUp':
- event.preventDefault();
- this.navigateHistory(-1);
- break;
- case 'ArrowDown':
- event.preventDefault();
- this.navigateHistory(1);
- break;
- case 'Tab':
- event.preventDefault();
- // Tab键插入Tab字符
- this.insertTabCharacter();
- break;
- }
- }
- // 发送原始数据到串口
- async sendRawData(data) {
- if (!this.isConnected || !this.port) {
- return;
- }
- try {
- const dataToSend = new TextEncoder().encode(data);
- if (!this.writer) {
- this.writer = this.port.writable.getWriter();
- }
- await this.writer.write(dataToSend);
- } catch (error) {
- this.addMessage(`发送数据失败: ${error.message}`, 'error');
- }
- }
- // 发送HEX格式数据
- async sendHexData(hexString) {
- if (!this.isConnected) {
- this.addMessage('请先连接串口', 'error');
- return;
- }
- try {
- // 清理输入:移除空格、换行符等
- const cleanHex = hexString.replace(/[\s\r\n]/g, '');
- // 验证是否为有效的十六进制字符串
- if (!/^[0-9A-Fa-f]*$/.test(cleanHex)) {
- this.addMessage('❌ 无效的十六进制数据,只能包含0-9和A-F字符', 'error');
- return;
- }
- // 确保是偶数长度(每个字节需要2个十六进制字符)
- const paddedHex = cleanHex.length % 2 === 0 ? cleanHex : '0' + cleanHex;
- if (paddedHex.length === 0) {
- this.addMessage('❌ 请输入有效的十六进制数据', 'error');
- return;
- }
- // 转换为字节数组
- const bytes = [];
- for (let i = 0; i < paddedHex.length; i += 2) {
- const byteValue = parseInt(paddedHex.substr(i, 2), 16);
- bytes.push(byteValue);
- }
- // 创建Uint8Array并发送
- const dataToSend = new Uint8Array(bytes);
- if (!this.writer) {
- this.writer = this.port.writable.getWriter();
- }
- await this.writer.write(dataToSend);
- // 显示发送的数据(格式化为易读的HEX格式)
- const formattedHex = paddedHex.toUpperCase().replace(/(.{2})/g, '$1 ').trim();
- this.addMessage(`HEX发送 (${bytes.length}字节): ${formattedHex}`, 'sent');
- } catch (error) {
- this.addMessage(`发送HEX数据失败: ${error.message}`, 'error');
- }
- }
- // 插入Tab字符(传统模式使用)
- insertTabCharacter() {
- const input = this.terminalInputField;
- const start = input.selectionStart;
- const end = input.selectionEnd;
- const value = input.value;
- const tabChar = '\t'; // 使用真实Tab字符
- const newValue = value.substring(0, start) + tabChar + value.substring(end);
- input.value = newValue;
- // 将光标移动到Tab字符之后
- const newCursorPos = start + tabChar.length;
- input.setSelectionRange(newCursorPos, newCursorPos);
- // 触发input事件以确保任何监听器都能收到通知
- input.dispatchEvent(new Event('input', { bubbles: true }));
- }
- executeTerminalCommand() {
- const command = this.terminalInputField.value.trim();
- if (!command) return;
- // 添加到命令历史
- if (this.commandHistory[this.commandHistory.length - 1] !== command) {
- this.commandHistory.push(command);
- // 限制历史记录数量
- if (this.commandHistory.length > 100) {
- this.commandHistory.shift();
- }
- }
- this.historyIndex = -1;
- if (this.hexModeCheckbox.checked) {
- // HEX模式:发送十六进制数据
- this.sendHexData(command);
- } else {
- // 文本模式:发送普通命令
- this.addMessage(`$ ${command}`, 'sent');
- this.sendCommand(command);
- }
- // 清空输入框
- this.terminalInputField.value = '';
- }
- navigateHistory(direction) {
- if (this.commandHistory.length === 0) return;
- if (direction === -1) {
- // 向上箭头 - 显示更早的命令
- if (this.historyIndex === -1) {
- this.historyIndex = this.commandHistory.length - 1;
- } else if (this.historyIndex > 0) {
- this.historyIndex--;
- }
- } else if (direction === 1) {
- // 向下箭头 - 显示更新的命令
- if (this.historyIndex === -1) {
- return;
- } else if (this.historyIndex < this.commandHistory.length - 1) {
- this.historyIndex++;
- } else {
- this.historyIndex = -1;
- this.terminalInputField.value = '';
- return;
- }
- }
- if (this.historyIndex >= 0 && this.historyIndex < this.commandHistory.length) {
- this.terminalInputField.value = this.commandHistory[this.historyIndex];
- // 将光标移到末尾
- setTimeout(() => {
- this.terminalInputField.setSelectionRange(
- this.terminalInputField.value.length,
- this.terminalInputField.value.length
- );
- }, 0);
- }
- }
- // 日志保存方法
- async saveLog() {
- try {
- if (this.logMessages.length === 0) {
- this.addMessage('没有日志内容可保存', 'info');
- return;
- }
- const logContent = this.generateLogContent();
- // 检查是否支持File System Access API
- if ('showSaveFilePicker' in window) {
- await this.saveLogWithFilePicker(logContent);
- } else {
- // 降级到传统下载方式
- this.saveLogWithDownload(logContent);
- }
- } catch (error) {
- if (error.name === 'AbortError') {
- this.addMessage('💡 用户取消了日志保存', 'info');
- } else {
- this.addMessage(`❌ 保存日志失败: ${error.message}`, 'error');
- }
- }
- }
- generateLogContent() {
- const lines = [];
- lines.push('# MicroLink Web Serial Terminal 日志');
- lines.push(`# 生成时间: ${new Date().toLocaleString()}`);
- lines.push(`# 总计消息数: ${this.logMessages.length}`);
- lines.push('# ========================================');
- lines.push('');
- for (const msg of this.logMessages) {
- const timestamp = msg.timestamp.toLocaleString();
- const typeLabel = this.getTypeLabel(msg.type);
- lines.push(`[${timestamp}] ${typeLabel} ${msg.content}`);
- }
- lines.push('');
- lines.push('# ========================================');
- lines.push('# 日志结束');
- return lines.join('\n');
- }
- getTypeLabel(type) {
- const labels = {
- 'sent': '[发送]',
- 'received': '[接收]',
- 'error': '[错误]',
- 'info': '[信息]'
- };
- return labels[type] || '[未知]';
- }
- async saveLogWithFilePicker(logContent) {
- try {
- const now = new Date();
- const dateStr = now.toISOString().slice(0, 19).replace(/[T:]/g, '-');
- const suggestedName = `microlink-log-${dateStr}.txt`;
- const fileHandle = await window.showSaveFilePicker({
- suggestedName: suggestedName,
- types: [
- {
- description: 'MicroLink日志文件',
- accept: {
- 'text/plain': ['.txt'],
- },
- },
- ],
- });
- const writable = await fileHandle.createWritable();
- await writable.write(logContent);
- await writable.close();
- this.addMessage('✅ 日志已保存到指定位置', 'info');
- } catch (error) {
- if (error.name === 'AbortError') {
- throw error;
- } else {
- this.addMessage('⚠️ 文件选择器不可用,使用下载方式保存', 'info');
- this.saveLogWithDownload(logContent);
- }
- }
- }
- saveLogWithDownload(logContent) {
- const now = new Date();
- const dateStr = now.toISOString().slice(0, 19).replace(/[T:]/g, '-');
- const filename = `microlink-log-${dateStr}.txt`;
- const blob = new Blob([logContent], { type: 'text/plain;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- this.addMessage(`✅ 日志已保存到下载文件夹 ${filename}`, 'info');
- }
- }
- // ========== YMODEM协议实现(移植自add.html,适配window.microLinkTerminal.port) ==========
- // CRC16校验计算 (用于YMODEM)
- function calculateCRC16(data) {
- let crc = 0x0000;
- const polynomial = 0x1021; // CRC-16-CCITT
- for (let i = 0; i < data.length; i++) {
- crc ^= (data[i] << 8);
- for (let j = 0; j < 8; j++) {
- if (crc & 0x8000) {
- crc = (crc << 1) ^ polynomial;
- } else {
- crc = crc << 1;
- }
- }
- crc &= 0xFFFF;
- }
- return crc;
- }
- // 调试函数:验证YMODEM数据包格式
- function debugYMODEMPacket(packet, packetType, blockNumber = 0) {
- const packetInfo = {
- type: packetType,
- blockNumber: blockNumber,
- totalLength: packet.length,
- header: {
- SOH: packet[0],
- blockNumber: packet[1],
- blockNumberComplement: packet[2]
- },
- dataArea: Array.from(packet.slice(3, 131)), // 128字节数据区(第4字节到第131字节)
- crc: {
- value: (packet[131] << 8) | packet[132]
- }
- };
-
- console.log(`=== YMODEM ${packetType} 包调试信息 ===`);
- console.log(`包类型: ${packetType}`);
- console.log(`包号: ${blockNumber}`);
- console.log(`总长度: ${packet.length} 字节`);
- console.log(`帧头: SOH=${packet[0].toString(16)}, 包号=${packet[1].toString(16)}, 反码=${packet[2].toString(16)}`);
- console.log(`数据区: 128字节(第4-131字节)`);
- console.log(`数据区前16字节: ${packetInfo.dataArea.slice(0, 16).map(b => b.toString(16).padStart(2, '0')).join(' ')}`);
- console.log(`数据区后16字节: ${packetInfo.dataArea.slice(-16).map(b => b.toString(16).padStart(2, '0')).join(' ')}`);
- console.log(`CRC值: ${packetInfo.crc.value.toString(16).padStart(4, '0')}`);
-
- // 验证协议规范
- const validations = [];
-
- // 验证帧头
- if (packet[0] === 0x01) validations.push('✓ SOH正确');
- else validations.push('✗ SOH错误');
-
- if (packet[1] === blockNumber) validations.push('✓ 包号正确');
- else validations.push('✗ 包号错误');
-
- if (packet[2] === (255 - blockNumber)) validations.push('✓ 包号反码正确');
- else validations.push('✗ 包号反码错误');
-
- // 验证数据区长度
- if (packetInfo.dataArea.length === 128) validations.push('✓ 数据区长度正确(128字节)');
- else validations.push('✗ 数据区长度错误');
-
- // 验证数据区内容
- if (packetType === '起始帧') {
- // 起始帧:文件名 + 0x00 + 文件大小 + 0x00 + 0x00填充
- const hasNullTerminators = packetInfo.dataArea.includes(0x00);
- if (hasNullTerminators) validations.push('✓ 包含NULL终止符');
- else validations.push('✗ 缺少NULL终止符');
- } else if (packetType === '数据帧') {
- // 数据帧:有效数据 + 0x1A填充
- const hasSubPadding = packetInfo.dataArea.slice(-10).some(b => b === 0x1A);
- if (hasSubPadding) validations.push('✓ 包含SUB填充(0x1A)');
- else validations.push('✗ 缺少SUB填充');
- } else if (packetType === '结束帧') {
- // 结束帧:全0x00
- const allZeros = packetInfo.dataArea.every(b => b === 0x00);
- if (allZeros) validations.push('✓ 数据区全为0x00');
- else validations.push('✗ 数据区不全为0x00');
- }
-
- // 验证包长度
- if (packet.length === 133) validations.push('✓ 包长度正确(133字节)');
- else validations.push('✗ 包长度错误');
-
- console.log('协议验证:', validations.join(', '));
- console.log('=====================================');
-
- return packetInfo;
- }
- // 修改后的数据包创建函数,全部使用CRC校验
- function createYMODEMHeaderPacket(fileName, fileSize) {
- const headerSize = 128;
- const packetSize = headerSize + 5; // 3字节帧头 + 128字节数据区 + 2字节CRC
- const header = new Uint8Array(packetSize);
-
- // 严格按照YMODEM协议规范
- header[0] = 0x01; // SOH - 起始帧固定使用SOH
- header[1] = 0x00; // 包号固定为0x00
- header[2] = 0xFF; // 包号反码 0xFF
-
- // 构建文件头信息:文件名 + 0x00 + 文件大小 + 0x00
- const headerInfo = `${fileName}\x00${fileSize}\x00`;
- const headerBytes = new TextEncoder().encode(headerInfo);
-
- // 复制文件头信息到数据区(从第4字节开始)
- header.set(headerBytes, 3);
-
- // 填充剩余字节为0x00(从文件信息结束到128字节数据区结束)
- // 数据区范围:第4字节到第131字节(共128字节)
- for (let i = headerBytes.length + 3; i < headerSize + 3; i++) {
- header[i] = 0x00;
- }
-
- // CRC模式:计算CRC16
- // 数据区:第4字节到第131字节(128字节)
- const dataForCRC = header.slice(3, headerSize + 3);
- const crc = calculateCRC16(dataForCRC);
- header[headerSize + 3] = (crc >> 8) & 0xFF; // CRC高字节
- header[headerSize + 4] = crc & 0xFF; // CRC低字节
-
- // 调试输出
- if (typeof console !== 'undefined' && console.log) {
- debugYMODEMPacket(header, '起始帧', 0);
- }
-
- return header;
- }
- function createYMODEMDataPacket(data, blockNumber) {
- const SOH_BLOCK_SIZE = 128; // SOH固定128字节
- const packetSize = SOH_BLOCK_SIZE + 5; // 3字节帧头 + 128字节数据区 + 2字节CRC
- const packet = new Uint8Array(packetSize);
-
- // 严格按照YMODEM协议规范
- packet[0] = 0x01; // SOH - 数据帧使用SOH(128字节块)
- packet[1] = blockNumber; // 包号
- packet[2] = 255 - blockNumber; // 包号反码
-
- // 复制有效数据到数据区(从第4字节开始)
- packet.set(data, 3);
-
- // 关键修正:SOH帧数据区严格为128字节,有效数据不足时用0x1A填充
- // 数据区范围:第4字节到第131字节(共128字节)
- for (let i = data.length + 3; i < SOH_BLOCK_SIZE + 3; i++) {
- packet[i] = 0x1A; // 用0x1A填充剩余字节
- }
-
- // CRC模式:计算CRC16
- // 数据区:第4字节到第131字节(128字节)
- const dataForCRC = packet.slice(3, SOH_BLOCK_SIZE + 3);
- const crc = calculateCRC16(dataForCRC);
- packet[SOH_BLOCK_SIZE + 3] = (crc >> 8) & 0xFF; // CRC高字节
- packet[SOH_BLOCK_SIZE + 4] = crc & 0xFF; // CRC低字节
-
- // 调试输出
- if (typeof console !== 'undefined' && console.log) {
- debugYMODEMPacket(packet, '数据帧', blockNumber);
- }
-
- return packet;
- }
- function createYMODEMEndPacket() {
- const headerSize = 128;
- const packetSize = headerSize + 5; // 3字节帧头 + 128字节数据区 + 2字节CRC
- const header = new Uint8Array(packetSize);
-
- // 严格按照YMODEM协议规范 - 结束帧
- header[0] = 0x01; // SOH - 结束帧固定使用SOH
- header[1] = 0x00; // 包号固定为0x00
- header[2] = 0xFF; // 包号反码 0xFF
-
- // 数据区全部填充0x00(空包)
- // 数据区范围:第4字节到第131字节(共128字节)
- for (let i = 3; i < headerSize + 3; i++) {
- header[i] = 0x00;
- }
-
- // CRC模式:计算CRC16
- // 数据区:第4字节到第131字节(128字节)
- const dataForCRC = header.slice(3, headerSize + 3);
- const crc = calculateCRC16(dataForCRC);
- header[headerSize + 3] = (crc >> 8) & 0xFF; // CRC高字节
- header[headerSize + 4] = crc & 0xFF; // CRC低字节
-
- // 调试输出
- if (typeof console !== 'undefined' && console.log) {
- debugYMODEMPacket(header, '结束帧', 0);
- }
-
- return header;
- }
- async function waitForStartSignal(reader, writer, onLog) {
- const timeout = 30000;
- const startTime = Date.now();
- let lastLogTime = 0;
- let consecutiveChars = 0;
- onLog && onLog('正在等待接收方启动信号...');
- while (Date.now() - startTime < timeout) {
- try {
- const { value, done } = await reader.read();
- if (done) break;
- if (value && value.length > 0) {
- for (let i = 0; i < value.length; i++) {
- const byte = value[i];
- if (byte === 0x15) return 'NAK';
- if (byte === 0x18) throw new Error('接收方取消了传输');
- if (byte === 0x43) return 'C';
- if (byte === 0x06) return 'ACK';
- if (byte === 0x2b) {
- consecutiveChars++;
- if (consecutiveChars >= 3) {
- await writer.write(new Uint8Array([0x15]));
- return 'PLUS_TRIGGER';
- }
- } else if (byte >= 0x20 && byte <= 0x7e) {
- consecutiveChars++;
- if (consecutiveChars >= 5) {
- await writer.write(new Uint8Array([0x15]));
- return 'CHAR_TRIGGER';
- }
- } else {
- consecutiveChars = 0;
- }
- }
- }
- } catch (error) {
- onLog && onLog('等待启动信号时出错: ' + error.message);
- }
- const currentTime = Date.now();
- if (currentTime - lastLogTime > 5000) {
- const elapsed = Math.round((currentTime - startTime) / 1000);
- onLog && onLog(`等待中... (${elapsed}s/${timeout/1000}s)`);
- lastLogTime = currentTime;
- }
- await new Promise(resolve => setTimeout(resolve, 100));
- }
- throw new Error('等待启动信号超时,请确保接收方已准备就绪');
- }
- async function sendYMODEMPacketWithACK(writer, reader, packet, blockNumber, onLog, maxRetries = 10, isHeaderPacket = false) {
- let retries = 0;
- let consecutiveErrors = 0; // 连续错误计数
-
- while (retries < maxRetries) {
- // 详细的包信息日志
- const packetType = blockNumber === 0 ? (packet.slice(3).every(b => b === 0) ? '结束帧' : '起始帧') : '数据帧';
- onLog && onLog(`准备发送${packetType} (包号: ${blockNumber}, 长度: ${packet.length}字节)`);
-
- // 验证包格式
- if (packet[0] !== 0x01) {
- onLog && onLog(`❌ 包格式错误: SOH应为0x01,实际为0x${packet[0].toString(16)}`);
- throw new Error('包格式错误: SOH不正确');
- }
-
- if (packet[1] !== blockNumber) {
- onLog && onLog(`❌ 包格式错误: 包号应为${blockNumber},实际为${packet[1]}`);
- throw new Error('包格式错误: 包号不正确');
- }
-
- if (packet[2] !== (255 - blockNumber)) {
- onLog && onLog(`❌ 包格式错误: 包号反码应为${255 - blockNumber},实际为${packet[2]}`);
- throw new Error('包格式错误: 包号反码不正确');
- }
-
- // 验证包长度
- const expectedLength = 133; // CRC模式固定133字节
- if (packet.length !== expectedLength) {
- onLog && onLog(`❌ 包长度错误: 应为${expectedLength}字节,实际为${packet.length}字节`);
- throw new Error('包长度错误');
- }
-
- try {
- await writer.write(packet);
- onLog && onLog(`✅ ${packetType}已发送,等待ACK...`);
-
- // 等待ACK
- await waitForACK(reader, onLog, isHeaderPacket);
- onLog && onLog(`✅ ${packetType}确认成功`);
- consecutiveErrors = 0; // 重置连续错误计数
- return;
-
- } catch (error) {
- consecutiveErrors++;
-
- if (error.message.includes('C字符')) {
- onLog && onLog('检测到C字符,设备端请求重启传输');
- throw new Error('RESTART_HEADER');
- } else if (error.message.includes('CAN') || error.message.includes('取消')) {
- onLog && onLog('检测到传输取消信号');
- throw new Error('TRANSMISSION_CANCELLED');
- } else if (error.message.includes('NAK')) {
- onLog && onLog('收到NAK,数据包校验失败');
- // NAK错误,继续重试
- } else if (error.message.includes('超时')) {
- onLog && onLog('等待ACK超时,设备端可能处理缓慢');
- // 超时错误,可能是设备端处理缓慢
- }
-
- retries++;
- onLog && onLog(`❌ ${packetType}确认失败,重试 ${retries}/${maxRetries} (连续错误: ${consecutiveErrors})`);
-
- if (retries >= maxRetries) {
- throw new Error(`${packetType}发送失败,超过最大重试次数`);
- }
-
- // 根据连续错误次数调整重试延时
- let retryDelay = 1000; // 基础延时1秒
- if (consecutiveErrors >= 3) {
- retryDelay = 3000; // 连续错误较多时,增加延时到3秒
- onLog && onLog(`连续错误较多,增加重试延时到${retryDelay}ms`);
- } else if (consecutiveErrors >= 5) {
- retryDelay = 5000; // 连续错误很多时,增加延时到5秒
- onLog && onLog(`连续错误很多,增加重试延时到${retryDelay}ms`);
- }
-
- onLog && onLog(`等待${retryDelay}ms后重试...`);
- await new Promise(resolve => setTimeout(resolve, retryDelay));
- }
- }
- }
- async function waitForACK(reader, onLog, isHeaderPacket = false) {
- const timeout = 15000; // 增加超时时间到15秒
- const startTime = Date.now();
- let buffer = '';
- while (Date.now() - startTime < timeout) {
- try {
- const { value, done } = await reader.read();
- if (done) {
- onLog && onLog('waitForACK: 串口流已关闭 (done=true)');
- break;
- }
- if (value && value.length > 0) {
- const text = new TextDecoder().decode(value);
- buffer += text;
- for (let i = 0; i < value.length; i++) {
- const byte = value[i];
- if (byte === 0x06) {
- onLog && onLog('收到ACK确认');
- return; // 成功收到ACK
- }
- if (byte === 0x15) {
- onLog && onLog('收到NAK,传输失败');
- throw new Error('收到NAK,传输失败');
- }
- if (byte === 0x18) {
- onLog && onLog('收到CAN,传输被取消');
- throw new Error('收到CAN,传输被取消');
- }
- if (byte === 0x43) {
- if (isHeaderPacket) {
- onLog && onLog('头包后收到C,协议正常,进入数据包1发送');
- return; // 头包后收到C,视为正常
- } else {
- onLog && onLog('收到C字符,需要切换到CRC模式或重启传输');
- throw new Error('收到C字符,需要切换到CRC模式');
- }
- }
- }
- if (onLog && value.length < 50) {
- const hexBytes = Array.from(value).map(b => b.toString(16).padStart(2, '0')).join(' ');
- onLog(`waitForACK: 收到内容: ${hexBytes}`);
- }
- }
- } catch (error) {
- if (error.message.includes('NAK') || error.message.includes('CAN') || error.message.includes('C字符')) {
- throw error;
- }
- onLog && onLog(`waitForACK读取错误: ${error.message}`);
- }
- await new Promise(resolve => setTimeout(resolve, 100));
- }
- throw new Error('等待ACK超时');
- }
- async function waitForACKOrNAK(reader, onLog) {
- const timeout = 10000;
- const startTime = Date.now();
- while (Date.now() - startTime < timeout) {
- try {
- const { value, done } = await reader.read();
- if (done) break;
- if (value && value.length > 0) {
- for (let i = 0; i < value.length; i++) {
- const byte = value[i];
- if (byte === 0x06) return 'ACK';
- if (byte === 0x15) return 'NAK';
- if (byte === 0x18) throw new Error('收到CAN,传输被取消');
- if (byte === 0x43) throw new Error('收到C字符,需要切换到CRC模式');
- }
- }
- } catch (error) {
- if (error.message.includes('CAN') || error.message.includes('C字符')) {
- throw error;
- }
- }
- await new Promise(resolve => setTimeout(resolve, 50));
- }
- throw new Error('等待ACK或NAK超时');
- }
- // 发送EOT并等待ACK/NAK/C/CAN,支持重试
- async function sendEOTWithACKRetry(writer, reader, config, onLog) {
- const eotTimeout = 1000; // EOT等待ACK最大1秒
- const maxEOTRetries = 5;
- for (let retry = 0; retry < maxEOTRetries; retry++) {
- onLog && onLog(`[DEBUG] 第${retry+1}次发送EOT信号...`);
- await writer.write(new Uint8Array([0x04]));
- onLog && onLog('发送EOT信号,等待ACK...');
- try {
- const resp = await waitForACKorC(reader, onLog, eotTimeout);
- if (resp === 'ACK') {
- onLog && onLog('EOT确认成功');
- return true;
- }
- if (resp === 'C') throw new Error('RESTART_HEADER');
- } catch (e) {
- if (e.message === 'RESTART_HEADER') throw e;
- onLog && onLog(`EOT未确认,重试${retry + 1}/${maxEOTRetries}`);
- await new Promise(r => setTimeout(r, config.retryDelay + retry * 200));
- }
- }
- onLog && onLog('❌ EOT多次重试失败,设备端未响应ACK,传输中止。');
- throw new Error('EOT多次重试失败');
- }
- // EOT发送函数:发送EOT后等待ACK,支持重试
- async function sendEOT(writer, reader, onLog, maxRetries = 5) {
- for (let retry = 0; retry < maxRetries; retry++) {
- onLog && onLog(`[DEBUG] 第${retry+1}次发送EOT信号...`);
- await writer.write(new Uint8Array([0x04]));
- onLog && onLog('发送EOT信号,等待ACK...');
- try {
- const resp = await waitForACKorC(reader, onLog, 1000); // 1秒超时
- if (resp === 'ACK') {
- onLog && onLog('EOT确认成功');
- return true;
- }
- if (resp === 'C') throw new Error('RESTART_HEADER');
- } catch (e) {
- if (e.message === 'RESTART_HEADER') throw e;
- onLog && onLog(`EOT未确认,重试${retry + 1}/${maxRetries}`);
- await new Promise(r => setTimeout(r, 200 + retry * 100));
- }
- }
- onLog && onLog('❌ EOT多次重试失败,设备端未响应ACK,传输中止。');
- throw new Error('EOT多次重试失败');
- }
- // waitForACKorC函数,支持自定义超时
- async function waitForACKorC(reader, onLog, timeout = 1000) {
- const startTime = Date.now();
- while (Date.now() - startTime < timeout) {
- try {
- const { value, done } = await reader.read();
- if (done) break;
- if (value && value.length > 0) {
- for (let i = 0; i < value.length; i++) {
- const byte = value[i];
- if (byte === 0x06) return 'ACK';
- if (byte === 0x15) return 'NAK';
- if (byte === 0x18) throw new Error('收到CAN,传输被取消');
- if (byte === 0x43) return 'C';
- }
- }
- } catch (error) {
- if (error.message.includes('CAN') || error.message.includes('C字符')) {
- throw error;
- }
- }
- await new Promise(resolve => setTimeout(resolve, 50));
- }
- throw new Error('等待ACK或C超时');
- }
- // 主入口:window.ymodemSendFileViaSerial
- window.ymodemSendFileViaSerial = async function(uint8Array, fileName, timeout, onProgress, onLog, options = {}) {
- const port = window.microLinkTerminal && window.microLinkTerminal.port;
- if (!port) throw new Error('串口未连接');
- const config = {
- retryDelay: options.retryDelay || 1000,
- maxRetries: options.maxRetries || 20,
- packetTimeout: options.packetTimeout || 15000,
- restartDelay: options.restartDelay || 2000,
- packetInterval: options.packetInterval || 1000,
- ...options
- };
- const SOH_BLOCK_SIZE = 128;
- let writer = null, reader = null;
- let restartCount = 0;
- const maxRestarts = 3;
- // === 头包构造函数优先用options.buildHeaderPacket ===
- const buildHeaderPacket = options.buildHeaderPacket || buildHeaderPacketYmodem;
- try {
- writer = port.writable.getWriter();
- reader = port.readable.getReader();
- onLog && onLog(`准备发送文件: ${fileName}, 大小: ${uint8Array.length} 字节`);
- onLog && onLog(`数据包大小: ${SOH_BLOCK_SIZE} 字节 (SOH)`);
- onLog && onLog(`预计数据包数量: ${Math.ceil(uint8Array.length / SOH_BLOCK_SIZE)}`);
- onLog && onLog(`包间延时: ${config.packetInterval}ms`);
- onLog && onLog(`包超时: ${config.packetTimeout}ms`);
- onLog && onLog(`校验方式: CRC16 (固定)`);
- await new Promise(r => setTimeout(r, 1000));
- await writer.write(new Uint8Array([0x00]));
- await new Promise(r => setTimeout(r, 200));
- await writer.write(new Uint8Array([0x43]));
- await new Promise(r => setTimeout(r, 200));
- onLog && onLog('已发送触发字符序列,等待接收方C信号...');
- while (restartCount <= maxRestarts) {
- try {
- await window.waitForCSignal(reader, onLog);
- await performYMODEMTransfer();
- onLog && onLog('YMODEM传输成功完成!');
- return;
- } catch (error) {
- // === 修正:EOT后收到C时直接发空头包,不再整体重启 ===
- if (error.message === 'RESTART_HEADER' && restartCount < maxRestarts) {
- onLog && onLog('EOT后收到C,直接发送空头包...');
- let endPacket = createYMODEMEndPacket();
- const endAck = await sendEndPacketWithRetry(writer, reader, endPacket, onLog, 10);
- if (!endAck) throw new Error('空头包多次重试失败,传输中止');
- return;
- } else if (error.message === 'RESTART_HEADER') {
- throw new Error('EOT后收到C,结束帧多次失败,传输中止');
- } else if (error.message === 'TRANSMISSION_CANCELLED') {
- throw new Error('传输被取消');
- } else {
- throw error;
- }
- }
- }
- throw new Error(`传输失败,已尝试${maxRestarts}次重启`);
- // 内部传输函数
- async function performYMODEMTransfer() {
- onLog && onLog('=== 开始YMODEM传输流程 ===');
- // 1. 发送起始帧
- let headerPacket = buildHeaderPacket(fileName, uint8Array.length);
- await sendPacketWithACKRetry(writer, reader, headerPacket, 0, config, onLog, '起始帧');
- onLog && onLog(`[DEBUG] 头包ACK后,准备延时${config.packetInterval}ms`);
- await new Promise(r => setTimeout(r, config.packetInterval)); // 头包和数据包1之间加延时
- onLog && onLog(`[DEBUG] 延时结束,准备发送数据包1`);
- // 2. 发送数据帧
- const totalBlocks = Math.ceil(uint8Array.length / SOH_BLOCK_SIZE);
- let transferred = 0;
- for (let blockNum = 1; blockNum <= totalBlocks; blockNum++) {
- const startIndex = (blockNum - 1) * SOH_BLOCK_SIZE;
- const endIndex = Math.min(startIndex + SOH_BLOCK_SIZE, uint8Array.length);
- const blockData = uint8Array.slice(startIndex, endIndex);
- // === 强制用buildYmodemPacketYmodem构造数据包 ===
- let dataPacket = buildYmodemPacketYmodem(blockNum, blockData);
- await sendPacketWithACKRetry(writer, reader, dataPacket, blockNum, config, onLog, `数据帧${blockNum}`);
- transferred += blockData.length;
- const progress = Math.round((transferred / uint8Array.length) * 100);
- onProgress && onProgress(progress);
- onLog && onLog(`✅ 数据帧${blockNum}/${totalBlocks} 传输完成 (${progress}%)`);
- onLog && onLog(`[DEBUG] 数据包${blockNum} ACK后,准备延时${config.packetInterval}ms`);
- await new Promise(r => setTimeout(r, config.packetInterval)); // 包间延时
- onLog && onLog(`[DEBUG] 延时结束,准备发送下一个数据包`);
- }
- // 3. 发送EOT,等待ACK
- await sendEOTWithACKRetry(writer, reader, config, onLog);
- await new Promise(r => setTimeout(r, config.packetInterval)); // 包间延时
- // === 关键:EOT和空头包之间增加延时 ===
- await new Promise(r => setTimeout(r, 500));
- // 4. 发送结束帧(空头包)
- let endPacket = createYMODEMEndPacket();
- const endAck = await sendEndPacketWithRetry(writer, reader, endPacket, onLog, 10);
- if (!endAck) throw new Error('空头包多次重试失败,传输中止');
- await new Promise(r => setTimeout(r, config.packetInterval)); // 包间延时
- // === 关键:结束包后严格等待ACK ===
- onLog && onLog('等待设备端ACK确认结束...');
- let gotAck = false;
- const ackStart = Date.now();
- while (Date.now() - ackStart < 5000) { // 最多等5秒
- const { value, done } = await Promise.race([
- reader.read(),
- new Promise(resolve => setTimeout(() => resolve({value: null, done: false}), 200))
- ]);
- if (value) {
- for (let i = 0; i < value.length; i++) {
- if (value[i] === 0x06) { // ACK
- gotAck = true;
- onLog('✅ 设备端ACK确认,YMODEM流程完成');
- break;
- }
- }
- if (gotAck) break;
- const text = new TextDecoder().decode(value);
- onLog('结束后收到内容: ' + text);
- }
- await new Promise(r => setTimeout(r, 100));
- }
- if (!gotAck) onLog('⚠️ 结束后未收到ACK,可能已完成也可能异常');
- onLog && onLog('✅ YMODEM传输流程完成');
- }
- } finally {
- if (writer) try { writer.releaseLock(); } catch (e) {}
- if (reader) try { reader.releaseLock(); } catch (e) {}
- }
- };
- // 简化的YMODEM发送函数,专门处理设备端问题
- window.ymodemSendFileViaSerialSimple = async function(uint8Array, fileName, timeout, onProgress, onLog, options = {}) {
- const port = window.microLinkTerminal && window.microLinkTerminal.port;
- if (!port) throw new Error('串口未连接');
- let writer = null, reader = null;
- // === 1. 先彻底暂停主终端监听 ===
- let wasConnected = false;
- window.isYmodemActive = true; // YMODEM流程期间屏蔽主终端
- if (window.microLinkTerminal) {
- // --- YMODEM前清空主终端缓冲 ---
- if (typeof window.microLinkTerminal.flushBuffer === 'function') {
- window.microLinkTerminal.flushBuffer();
- }
- wasConnected = window.microLinkTerminal.isConnected;
- window.microLinkTerminal.isConnected = false;
- if (window.microLinkTerminal.reader) {
- try { await window.microLinkTerminal.reader.cancel(); } catch (e) {}
- try { window.microLinkTerminal.reader.releaseLock(); } catch (e) {}
- window.microLinkTerminal.reader = null;
- }
- // 等待主终端读取循环彻底退出
- await new Promise(r => setTimeout(r, 300));
- }
- // === 头包构造函数优先用options.buildHeaderPacket ===
- const buildHeaderPacket = options.buildHeaderPacket || buildHeaderPacketYmodem;
- try {
- // === 2. 再获取writer/reader并发送ym.receive()指令 ===
- writer = port.writable.getWriter();
- reader = port.readable.getReader();
- onLog && onLog('发送 ym.receive() 指令...');
- await writer.write(new TextEncoder().encode('ym.receive()\n'));
- // 2. 等待接收端发送 'C'
- onLog && onLog('等待接收端发送 "C"...');
- if (!(await waitForByteYmodem(reader, 0x43, 10000, onLog))) {
- onLog && onLog('未收到接收端 "C",发送中止');
- throw new Error('未收到接收端 "C"');
- }
- // 3. 发送头包
- onLog && onLog('发送 Ymodem 文件头包...');
- if (!(await sendAndWaitAckYmodem(writer, reader, buildHeaderPacket(fileName, uint8Array.length), onLog))) {
- onLog && onLog('头包发送失败,发送中止');
- throw new Error('头包发送失败');
- }
- // 4. 等待接收端再次发送 'C'
- onLog && onLog('等待接收端再次发送 "C"...');
- if (!(await waitForByteYmodem(reader, 0x43, 10000, onLog))) {
- onLog && onLog('未收到接收端第二个 "C",发送中止');
- throw new Error('未收到接收端第二个 "C"');
- }
- // 5. 发送数据包
- let seq = 1;
- for (let offset = 0; offset < uint8Array.length; offset += 128) {
- let chunk = uint8Array.slice(offset, offset + 128);
- // === 强制用buildYmodemPacketYmodem构造数据包 ===
- if (!(await sendAndWaitAckYmodem(writer, reader, buildYmodemPacketYmodem(seq, chunk), onLog))) {
- onLog && onLog(`数据包 #${seq} 发送失败,发送中止`);
- throw new Error(`数据包 #${seq} 发送失败`);
- }
- seq++;
- if (onProgress) onProgress(Math.round((offset + chunk.length) / uint8Array.length * 100));
- }
- // 6. 发送EOT,等待ACK
- onLog && onLog('发送 EOT...');
- for (let i = 0; i < 10; i++) {
- await writer.write(new Uint8Array([0x04]));
- let b = await readByteYmodem(reader, 3000, onLog);
- if (b === 0x06) break;
- }
- // 7. 等待接收端发送 'C'
- onLog && onLog('等待接收端发送 "C"...');
- if (!(await waitForByteYmodem(reader, 0x43, 10000, onLog))) {
- onLog && onLog('未收到接收端最后一个 "C",发送中止');
- throw new Error('未收到接收端最后一个 "C"');
- }
- // 8. 发送空头包
- onLog && onLog('发送空头包...');
- if (!(await sendEndPacketWithRetry(writer, reader, buildEndPacketYmodem(), onLog, 10))) {
- onLog && onLog('空头包发送失败');
- throw new Error('空头包发送失败');
- }
- onLog && onLog('文件发送完成!');
- } finally {
- if (writer) try { writer.releaseLock(); } catch (e) {}
- if (reader) try { reader.releaseLock(); } catch (e) {}
- // === 恢复主终端监听 ===
- if (window.microLinkTerminal) {
- window.microLinkTerminal.isConnected = wasConnected;
- if (wasConnected) window.microLinkTerminal.startReading();
- }
- window.isYmodemActive = false; // 恢复主终端
- }
- };
- function crc16_ccitt(buf) {
- let crc = 0x0000;
- for (let i = 0; i < buf.length; i++) {
- crc ^= (buf[i] << 8);
- for (let j = 0; j < 8; j++) {
- if (crc & 0x8000) {
- crc = (crc << 1) ^ 0x1021;
- } else {
- crc = crc << 1;
- }
- crc &= 0xFFFF;
- }
- }
- return crc;
- }
- function buildYmodemPacketYmodem(seq, data) {
- let packet = new Uint8Array(133);
- packet[0] = 0x01; // SOH
- packet[1] = seq & 0xFF;
- packet[2] = (~seq) & 0xFF;
- for (let i = 0; i < 128; i++) {
- packet[3 + i] = data[i] !== undefined ? data[i] : 0x1A;
- }
- let crc = crc16_ccitt(packet.slice(3, 131));
- packet[131] = (crc >> 8) & 0xFF;
- packet[132] = crc & 0xFF;
- return packet;
- }
- // === 极简YMODEM头包构造(前缀逻辑保持不变,外部传入name) ===
- function buildHeaderPacketYmodem(name, size) {
- // 检查是否为FLM文件,如果是则自动加前缀
- if (name && !name.startsWith('FLM/') && name.endsWith('.FLM.o')) {
- name = 'FLM/' + name;
- }
- let data = new Uint8Array(128);
- let nameBytes = new TextEncoder().encode(name);
- data.set(nameBytes, 0);
- let sizeBytes = new TextEncoder().encode(size.toString());
- data.set(sizeBytes, nameBytes.length + 1);
- return buildYmodemPacketYmodem(0, data);
- }
- // === 极简YMODEM结束包构造 ===
- function buildEndPacketYmodem() {
- let data = new Uint8Array(128);
- return buildYmodemPacketYmodem(0, data);
- }
- // === 极简YMODEM主流程(可被各tab直接调用) ===
- window.sendFileViaYmodem = async function(port, fileBuffer, fileName, fileSize, onLog) {
- let writer = port.writable.getWriter();
- let reader = port.readable.getReader();
- function log(msg) { onLog && onLog(msg); }
- // 1. 发送ym.receive()
- log('发送 ym.receive() 指令...');
- await writer.write(new TextEncoder().encode('ym.receive()\n'));
- // 2. 等待C
- log('等待接收端发送 "C"...');
- if (!(await waitForByteYmodem(reader, 0x43, 10000, log))) {
- log('未收到接收端 "C",发送中止');
- return false;
- }
- // 3. 发送头包
- log('发送 Ymodem 文件头包...');
- if (!(await sendAndWaitAckYmodem(writer, reader, buildHeaderPacketYmodem(fileName, fileSize), log))) {
- log('头包发送失败,发送中止');
- return false;
- }
- // 4. 等待C
- log('等待接收端再次发送 "C"...');
- if (!(await waitForByteYmodem(reader, 0x43, 10000, log))) {
- log('未收到接收端第二个 "C",发送中止');
- return false;
- }
- // 5. 发送数据包
- let seq = 1;
- for (let offset = 0; offset < fileBuffer.length; offset += 128) {
- let chunk = fileBuffer.slice(offset, offset + 128);
- log(`发送数据包 #${seq}...`);
- if (!(await sendAndWaitAckYmodem(writer, reader, buildYmodemPacketYmodem(seq, chunk), log))) {
- log(`数据包 #${seq} 发送失败,发送中止`);
- return false;
- }
- seq++;
- }
- // 6. 发送EOT
- log('发送 EOT...');
- for (let i = 0; i < 10; i++) {
- await writer.write(new Uint8Array([0x04]));
- let b = await readByteYmodem(reader, 3000, log);
- if (b === 0x06) break;
- }
- // 7. 等待C
- log('等待接收端发送 "C"...');
- if (!(await waitForByteYmodem(reader, 0x43, 10000, log))) {
- log('未收到接收端最后一个 "C",发送中止');
- return false;
- }
- // 8. 发送空头包
- log('发送空头包...');
- if (!(await sendEndPacketWithRetry(writer, reader, buildEndPacketYmodem(), log))) {
- log('空头包发送失败');
- return false;
- }
- log('文件发送完成!');
- writer.releaseLock();
- reader.releaseLock();
- return true;
- }
- async function readByteYmodem(reader, timeout = 3000, onLog) {
- const timer = setTimeout(() => reader.cancel(), timeout);
- try {
- const { value } = await reader.read();
- clearTimeout(timer);
- return value ? value[0] : null;
- } catch {
- clearTimeout(timer);
- return null;
- }
- }
- async function waitForByteYmodem(reader, target, timeout = 10000, onLog) {
- let start = Date.now();
- while (Date.now() - start < timeout) {
- let b = await readByteYmodem(reader, timeout, onLog);
- if (b === target) return true;
- }
- return false;
- }
- async function sendAndWaitAckYmodem(writer, reader, packet, onLog, retry = 10) {
- for (let i = 0; i < retry; i++) {
- await writer.write(packet);
- let b = await readByteYmodem(reader, 3000, onLog);
- if (b === 0x06) return true; // ACK
- if (b === 0x15) continue; // NAK
- }
- return false;
- }
- // 测试函数:验证YMODEM数据包格式
- window.testYMODEMPacketFormat = function() {
- console.log('=== YMODEM数据包格式测试(CRC模式) ===');
-
- // 测试起始帧
- console.log('\n1. 测试起始帧格式');
- const headerPacket = createYMODEMHeaderPacket('test.bin', 1024);
- debugYMODEMPacket(headerPacket, '起始帧', 0);
-
- // 验证起始帧长度
- console.log(`起始帧长度验证: ${headerPacket.length}字节 (期望: 133字节)`);
- console.log(`数据区长度验证: ${headerPacket.slice(3, 131).length}字节 (期望: 128字节)`);
-
- // 测试数据帧 - 完整数据
- console.log('\n2. 测试数据帧格式(完整数据)');
- const fullData = new Uint8Array(128);
- for (let i = 0; i < 128; i++) {
- fullData[i] = i;
- }
- const fullDataPacket = createYMODEMDataPacket(fullData, 1);
- debugYMODEMPacket(fullDataPacket, '数据帧(完整)', 1);
-
- // 测试数据帧 - 部分数据(需要填充)
- console.log('\n3. 测试数据帧格式(部分数据,需要0x1A填充)');
- const partialData = new Uint8Array(64);
- for (let i = 0; i < 64; i++) {
- partialData[i] = i;
- }
- const partialDataPacket = createYMODEMDataPacket(partialData, 2);
- debugYMODEMPacket(partialDataPacket, '数据帧(部分)', 2);
-
- // 验证填充逻辑
- const dataArea = partialDataPacket.slice(3, 131);
- const originalData = dataArea.slice(0, 64);
- const paddingData = dataArea.slice(64);
- console.log(`原始数据长度: ${originalData.length}字节`);
- console.log(`填充数据长度: ${paddingData.length}字节`);
- console.log(`填充数据是否全为0x1A: ${paddingData.every(b => b === 0x1A)}`);
-
- // 测试结束帧
- console.log('\n4. 测试结束帧格式');
- const endPacket = createYMODEMEndPacket();
- debugYMODEMPacket(endPacket, '结束帧', 0);
-
- // 验证结束帧数据区
- const endDataArea = endPacket.slice(3, 131);
- console.log(`结束帧数据区是否全为0x00: ${endDataArea.every(b => b === 0x00)}`);
-
- console.log('\n=== 测试完成 ===');
- console.log('关键验证点:');
- console.log('- 所有SOH帧数据区严格为128字节');
- console.log('- 数据帧不足128字节时用0x1A填充');
- console.log('- 所有包使用CRC16校验');
- console.log('- 所有包长度133字节');
-
- return true;
- };
- // 页面加载完成后初始化
- document.addEventListener('DOMContentLoaded', () => {
- window.microLinkTerminal = new MicroLinkTerminal();
-
- // 在开发模式下自动运行测试
- if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
- console.log('开发模式:运行YMODEM数据包格式测试');
- setTimeout(() => {
- window.testYMODEMPacketFormat();
- }, 1000);
- }
- });
- // --- handleReceivedData 屏蔽逻辑 ---
- const origHandleReceivedData = MicroLinkTerminal.prototype.handleReceivedData;
- MicroLinkTerminal.prototype.handleReceivedData = function(data) {
- if (window.isYmodemActive) return;
- // 自动接入变量分析曲线
- // 旧的handleRealtimeHexData已删除,现在使用Worker方式
- return origHandleReceivedData.call(this, data);
- };
- // --- handleReceivedData 屏蔽逻辑 ---
- // --- flushBuffer 屏蔽逻辑 ---
- const origFlushBuffer = MicroLinkTerminal.prototype.flushBuffer;
- MicroLinkTerminal.prototype.flushBuffer = function() {
- if (window.isYmodemActive) return;
- return origFlushBuffer.call(this);
- };
- // --- flushBuffer 屏蔽逻辑 ---
- // --- startReading 屏蔽逻辑 ---
- const origStartReading = MicroLinkTerminal.prototype.startReading;
- MicroLinkTerminal.prototype.startReading = function() {
- if (window.isYmodemActive) return; // YMODEM期间禁止重启监听
- return origStartReading.call(this);
- };
- // --- startReading 屏蔽逻辑 ---
- function waitForCSignal(reader, onLog) {
- // ...原有实现...
- }
- window.waitForCSignal = waitForCSignal;
- function sendPacketWithACKRetry(writer, reader, packet, blockNumber, config, onLog, packetType, isHeaderPacket) {
- // ...原有实现...
- }
- window.sendPacketWithACKRetry = sendPacketWithACKRetry;
- // === 关键:结束包后严格等待ACK,超时重发,最多10次 ===
- async function sendEndPacketWithRetry(writer, reader, endPacket, onLog, maxRetry = 10) {
- for (let i = 0; i < maxRetry; i++) {
- await writer.write(endPacket);
- onLog && onLog(`发送空头包(第${i+1}次),等待ACK...`);
- let gotAck = false;
- const ackStart = Date.now();
- while (Date.now() - ackStart < 5000) { // 最多等5秒
- const { value, done } = await Promise.race([
- reader.read(),
- new Promise(resolve => setTimeout(() => resolve({value: null, done: false}), 200))
- ]);
- if (value) {
- for (let j = 0; j < value.length; j++) {
- if (value[j] === 0x06) { // ACK
- gotAck = true;
- onLog && onLog('✅ 设备端ACK确认,YMODEM流程完成');
- return true;
- }
- }
- }
- await new Promise(r => setTimeout(r, 100));
- }
- onLog && onLog(`⚠️ 空头包第${i+1}次未收到ACK,准备重发...`);
- }
- onLog && onLog('❌ 空头包多次重试失败,设备端未响应ACK,传输中止。');
- return false;
- }
- // 多文件配置功能
- let fileTableBody, addFileBtn, clearFilesBtn;
- // 确保DOM加载完成后再初始化
- function initMultiFileConfig() {
- fileTableBody = document.getElementById('fileTableBody');
- addFileBtn = document.getElementById('addFileBtn');
- clearFilesBtn = document.getElementById('clearFilesBtn');
-
- if (!fileTableBody || !addFileBtn || !clearFilesBtn) {
- console.error('多文件配置元素未找到,延迟初始化...');
- setTimeout(initMultiFileConfig, 100);
- return;
- }
-
- // 初始化表格
- initFileTable();
-
- // 事件监听器
- addFileBtn.addEventListener('click', function() {
- console.log('添加文件按钮被点击');
- addFileRow();
- if (window.updateCodePreview) {
- window.updateCodePreview();
- }
- });
-
- // 测试按钮是否正常工作
- console.log('多文件配置初始化完成', {
- fileTableBody: !!fileTableBody,
- addFileBtn: !!addFileBtn,
- clearFilesBtn: !!clearFilesBtn
- });
-
- clearFilesBtn.addEventListener('click', function() {
- if (confirm('确定要清空所有文件配置吗?')) {
- clearAllFiles();
- }
- });
- }
- // 初始化文件表格
- function initFileTable() {
- if (!fileTableBody) return;
- fileTableBody.innerHTML = '';
- if (window.config && window.config.files) {
- window.config.files.forEach((file, index) => {
- addFileRow(file, index);
- });
- }
- }
- // 添加文件行
- function addFileRow(file = null, index = null) {
- console.log('添加文件行被调用', { file, index });
- if (!fileTableBody) {
- console.error('fileTableBody 未找到');
- return;
- }
- const row = document.createElement('div');
- row.className = 'file-table-row';
- row.dataset.index = index !== null ? index : (window.config && window.config.files ? window.config.files.length : 0);
-
- const fileNameInput = document.createElement('input');
- fileNameInput.type = 'text';
- fileNameInput.placeholder = '例如: boot.bin';
- fileNameInput.value = file ? file.fileName : '';
-
- const addressInput = document.createElement('input');
- addressInput.type = 'text';
- addressInput.placeholder = '例如: 0x08000000';
- addressInput.value = file ? file.address : '';
-
- const algorithmInput = document.createElement('input');
- algorithmInput.type = 'text';
- algorithmInput.placeholder = '例如: STM32F7x_1024.FLM.o';
- algorithmInput.value = file ? file.algorithm : '';
-
- const deleteBtn = document.createElement('button');
- deleteBtn.className = 'delete-file-btn';
- deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
- deleteBtn.title = '删除此行';
-
- row.appendChild(fileNameInput);
- row.appendChild(addressInput);
- row.appendChild(algorithmInput);
- row.appendChild(deleteBtn);
-
- fileTableBody.appendChild(row);
-
- // 添加事件监听器
- fileNameInput.addEventListener('input', function() {
- updateFileConfig();
- });
- addressInput.addEventListener('input', function() {
- updateFileConfig();
- });
- algorithmInput.addEventListener('input', function() {
- updateFileConfig();
- });
- deleteBtn.addEventListener('click', function() {
- deleteFileRow(row);
- });
-
- // 如果是新行,添加到配置中
- if (!file && window.config && window.config.files) {
- window.config.files.push({
- fileName: '',
- address: '',
- algorithm: ''
- });
- }
- }
- // 删除文件行
- function deleteFileRow(row) {
- if (!fileTableBody || !row) return;
- const index = parseInt(row.dataset.index);
- if (window.config && window.config.files && index >= 0 && index < window.config.files.length) {
- window.config.files.splice(index, 1);
- }
- fileTableBody.removeChild(row);
- updateRowIndices();
- if (window.updateCodePreview) {
- window.updateCodePreview();
- }
- }
- // 更新行索引
- function updateRowIndices() {
- if (!fileTableBody) return;
- const rows = fileTableBody.querySelectorAll('.file-table-row');
- rows.forEach((row, index) => {
- row.dataset.index = index;
- });
- }
- // 更新文件配置
- function updateFileConfig() {
- if (!fileTableBody || !window.config || !window.config.files) return;
- const rows = fileTableBody.querySelectorAll('.file-table-row');
- window.config.files = [];
- rows.forEach(row => {
- const inputs = row.querySelectorAll('input');
- if (inputs.length >= 3) {
- window.config.files.push({
- fileName: inputs[0].value,
- address: inputs[1].value,
- algorithm: inputs[2].value
- });
- }
- });
- if (window.updateCodePreview) {
- window.updateCodePreview();
- }
- }
- // 清空所有文件
- function clearAllFiles() {
- if (!fileTableBody) return;
- fileTableBody.innerHTML = '';
- if (window.config && window.config.files) {
- window.config.files.length = 0;
- }
- if (window.updateCodePreview) {
- window.updateCodePreview();
- }
- }
- // 三栏布局:sidebar切换逻辑
- function setupSidebarPanelSwitch() {
- const sidebarBtns = document.querySelectorAll('.sidebar-btn');
- const panels = ['serialPanel', 'flmPanel', 'scriptPanel', 'varPanel'];
- const monitorPanel = document.querySelector('.monitor-panel');
- sidebarBtns.forEach(btn => {
- btn.addEventListener('click', function() {
- // 切换按钮active
- sidebarBtns.forEach(b => b.classList.remove('active'));
- this.classList.add('active');
- // 切换内容区
- panels.forEach(pid => {
- const panel = document.getElementById(pid);
- if (panel) panel.style.display = (this.dataset.panel === pid) ? 'block' : 'none';
- });
-
- // 控制右侧监控面板的显示/隐藏
- if (monitorPanel) {
- if (this.dataset.panel === 'varPanel') {
- // 变量分析页面:隐藏监控面板
- monitorPanel.style.display = 'none';
- } else {
- // 其他页面:显示监控面板
- monitorPanel.style.display = '';
- }
- }
-
- // 切换到Python脚本配置时初始化
- if (this.dataset.panel === 'scriptPanel') {
- setupPythonScriptPanel();
- // 初始化多文件配置
- initMultiFileConfig();
- }
- // 切换到变量分析时初始化
- if (this.dataset.panel === 'varPanel') {
- setupVarAnalysisPanel();
- }
- });
- });
- }
- document.addEventListener('DOMContentLoaded', function() {
- setupSidebarPanelSwitch();
- });
- // ... 现有代码 ...
- function setupPythonScriptPanel() {
- // 防止重复绑定
- if (window._pythonPanelInited) return;
- window._pythonPanelInited = true;
- const swdClockSpeedMap = { '10M': '10000000', '5M': '5000000', '2M': '2000000', '1M': '1000000', '500K': '500000', '200K': '200000', '100K': '100000', '50K': '50000', '20K': '20000', '10K': '10000', '5K': '5000' };
- const customFlmInput = document.getElementById('customFlm');
- const address1Input = document.getElementById('address1');
- const address2Input = document.getElementById('address2');
- const binFileNameInput = document.getElementById('binFileName');
- const swdClockSpeedSelect = document.getElementById('swdClockSpeed');
- const codePreview = document.getElementById('codePreview');
- const dragCodePreview = document.getElementById('dragCodePreview');
- // 删除下载按钮相关代码
- const pyYmodemSendBtn = document.getElementById('pyYmodemSendBtn');
- const pyYmodemProgress = document.getElementById('pyYmodemProgress');
- const pyYmodemLog = document.getElementById('pyYmodemLog');
- // 使用全局config对象
- if (typeof window.config === 'undefined') {
- window.config = {
- flmFile: (customFlmInput && customFlmInput.value) || 'custom_flm.FLM.o',
- address1: (address1Input && address1Input.value) || '0X08000000',
- address2: (address2Input && address2Input.value) || '0x20000000',
- binFileName: (binFileNameInput && binFileNameInput.value) || 'ILI9341_HAL.bin',
- swdClockSpeed: (swdClockSpeedSelect && swdClockSpeedSelect.value) || '10M',
- files: [
- {
- fileName: 'boot.bin',
- address: '0x08000000',
- algorithm: 'STM32F7x_1024.FLM.o'
- },
- {
- fileName: 'rtthread.bin',
- address: '0x08020000',
- algorithm: 'STM32F7x_1024.FLM.o'
- },
- {
- fileName: 'HZK.bin',
- address: '0x90000000',
- algorithm: 'STM32F767_W25QXX.FLM.o'
- }
- ]
- };
- }
- const config = window.config;
-
- // 确保config对象有files数组
- if (!config.files) {
- config.files = [
- {
- fileName: 'boot.bin',
- address: '0x08000000',
- algorithm: 'STM32F7x_1024.FLM.o'
- },
- {
- fileName: 'rtthread.bin',
- address: '0x08020000',
- algorithm: 'STM32F7x_1024.FLM.o'
- },
- {
- fileName: 'HZK.bin',
- address: '0x90000000',
- algorithm: 'STM32F767_W25QXX.FLM.o'
- }
- ];
- }
- // 全局updateCodePreview函数
- window.updateCodePreview = function() {
- const flmFile = config.flmFile;
- const pythonSwdSpeed = swdClockSpeedMap[config.swdClockSpeed] || '10000000';
-
- // 生成多文件烧录代码
- 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()`;
-
- // 按算法分组文件
- const algorithmGroups = {};
- if (config.files && config.files.length > 0) {
- config.files.forEach(file => {
- if (file.algorithm && file.fileName && file.address) {
- if (!algorithmGroups[file.algorithm]) {
- algorithmGroups[file.algorithm] = [];
- }
- algorithmGroups[file.algorithm].push(file);
- }
- });
-
- // 为每个算法生成加载和烧录代码
- Object.keys(algorithmGroups).forEach((algorithm, index) => {
- const files = algorithmGroups[algorithm];
- if (files.length > 0) {
- // 加载算法
- offlineCode += `\n# 加载 ${algorithm} 下载算法文件\nresult = ReadFlm.load("FLM/${algorithm}", ${config.address1}, ${config.address2})\nif result != 0:\n return`;
-
- // 烧录该算法下的所有文件
- files.forEach(file => {
- offlineCode += `\n\n# 烧写 ${file.fileName}\nresult = load.bin("${file.fileName}", ${file.address})\nif result != 0:\n return`;
- });
- }
- });
- } else {
- // 如果没有多文件配置,使用默认的单文件配置
- 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`;
- }
-
- offlineCode += `\n\n# 蜂鸣器响一声,表示烧写完成\nbuzzer.enable()\nbuzzer.high()\ntime.sleep_ms(500)\nbuzzer.low()\ntime.sleep_ms(500)`;
-
- const dragCode = `import FLMConfig\ncmd.set_swd_clock(${pythonSwdSpeed})\nReadFlm = FLMConfig.ReadFlm()\nres1 = ReadFlm.load("FLM/${flmFile}",${config.address1},${config.address2})`;
-
- if (codePreview) {
- codePreview.textContent = offlineCode;
- }
- if (dragCodePreview) {
- dragCodePreview.textContent = dragCode;
- }
-
- // 高亮显示
- let highlightedCode = offlineCode;
- if (config.files && config.files.length > 0) {
- config.files.forEach(file => {
- if (file.fileName) {
- highlightedCode = highlightedCode.replace(
- new RegExp(`"${file.fileName}"`, 'g'),
- `<span class="highlight">"${file.fileName}"</span>`
- );
- }
- if (file.address) {
- highlightedCode = highlightedCode.replace(
- new RegExp(file.address.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'),
- `<span class="highlight">${file.address}</span>`
- );
- }
- if (file.algorithm) {
- highlightedCode = highlightedCode.replace(
- new RegExp(`"FLM/${file.algorithm}"`, 'g'),
- `<span class="highlight">"FLM/${file.algorithm}"</span>`
- );
- }
- });
- }
-
- // 高亮其他配置项
- highlightedCode = highlightedCode.replace(`"FLM/${flmFile}"`, `<span class="highlight">"FLM/${flmFile}"</span>`)
- .replace(new RegExp(config.address1.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${config.address1}</span>`)
- .replace(new RegExp(config.address2.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${config.address2}</span>`)
- .replace(new RegExp(pythonSwdSpeed.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${pythonSwdSpeed}</span>`);
-
- if (codePreview) {
- codePreview.innerHTML = highlightedCode;
- }
- if (dragCodePreview) {
- dragCodePreview.innerHTML = dragCode.replace(`"FLM/${flmFile}"`, `<span class="highlight">"FLM/${flmFile}"</span>`)
- .replace(new RegExp(config.address1.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${config.address1}</span>`)
- .replace(new RegExp(config.address2.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${config.address2}</span>`)
- .replace(new RegExp(pythonSwdSpeed.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'), 'g'), `<span class="highlight">${pythonSwdSpeed}</span>`);
- }
- }
- if (customFlmInput) {
- customFlmInput.addEventListener('input', function() { config.flmFile = this.value || 'custom_flm.FLM.o'; window.updateCodePreview(); });
- }
- if (address1Input) {
- address1Input.addEventListener('input', function() { config.address1 = this.value || '0X08000000'; window.updateCodePreview(); });
- }
- if (address2Input) {
- address2Input.addEventListener('input', function() { config.address2 = this.value || '0x20000000'; window.updateCodePreview(); });
- }
- if (binFileNameInput) {
- binFileNameInput.addEventListener('input', function() { config.binFileName = this.value || 'firmware.bin'; window.updateCodePreview(); });
- }
- if (swdClockSpeedSelect) {
- swdClockSpeedSelect.addEventListener('change', function() { config.swdClockSpeed = this.value; window.updateCodePreview(); });
- }
- // 删除下载按钮事件监听器
- // YMODEM发送按钮状态和事件
- function updatePyYmodemSendBtnState() {
- const tab = document.querySelector('.script-tab.active');
- if (tab && tab.textContent.includes('离线')) {
- pyYmodemSendBtn.disabled = false;
- pyYmodemSendBtn.setAttribute('data-pytype', 'offline');
- } else if (tab && tab.textContent.includes('拖拽')) {
- pyYmodemSendBtn.disabled = false;
- pyYmodemSendBtn.setAttribute('data-pytype', 'drag');
- } else {
- pyYmodemSendBtn.disabled = true;
- pyYmodemSendBtn.removeAttribute('data-pytype');
- }
- }
- const scriptTabs = document.querySelectorAll('.script-tab');
- if (scriptTabs.length > 0) {
- scriptTabs.forEach(tab => {
- tab.addEventListener('click', updatePyYmodemSendBtnState);
- });
- updatePyYmodemSendBtnState();
- }
- // Python日志输出到主终端
- // Python日志输出到主终端
- function pyYlog(msg, color) {
- appendToTerminalOutput(`<div class='log-prefix-python'>[PYTHON] ${msg}</div>`);
- }
- function pyYlogClear() {
- // 不清空主终端
- }
-
- // 统一的Python YMODEM发送函数
- async function sendPythonYmodem(code, fileName) {
- if (!window.microLinkTerminal || !window.microLinkTerminal.isConnected || !window.microLinkTerminal.port) {
- pyYlog('请先连接串口', '#f66'); return;
- }
- pyYmodemSendOfflineBtn && (pyYmodemSendOfflineBtn.disabled = true);
- pyYmodemSendDragBtn && (pyYmodemSendDragBtn.disabled = true);
- pyYlogClear();
- pyYlog('准备发送...', '#0ff');
- let wasConnected = false;
- if (window.microLinkTerminal) {
- if (window.microLinkTerminal.reader) {
- try { window.microLinkTerminal.reader.cancel(); } catch(e){}
- try { window.microLinkTerminal.reader.releaseLock(); } catch(e){}
- window.microLinkTerminal.reader = null;
- }
- wasConnected = window.microLinkTerminal.isConnected;
- window.microLinkTerminal.isConnected = false;
- await new Promise(r => setTimeout(r, 300));
- }
- try {
- const uint8Array = new TextEncoder().encode(code);
- const port = window.microLinkTerminal && window.microLinkTerminal.port;
- if (!port) throw new Error('串口未连接');
- let ok = await window.sendFileViaYmodem(
- port,
- uint8Array,
- fileName,
- uint8Array.length,
- msg => pyYlog(msg)
- );
- if (ok) {
- pyYlog('✅ 发送完成', '#0f0');
- } else {
- pyYlog('❌ 发送失败', '#f66');
- }
- } catch (e) {
- pyYlog('❌ 发送失败: ' + e.message, '#f66');
- if (e && e.stack) pyYlog('错误堆栈: ' + e.stack, '#f66');
- } finally {
- if (window.microLinkTerminal) {
- window.microLinkTerminal.isConnected = wasConnected;
- if (wasConnected && typeof window.microLinkTerminal.startReading === 'function') {
- window.microLinkTerminal.startReading();
- }
- }
- pyYmodemSendOfflineBtn && (pyYmodemSendOfflineBtn.disabled = false);
- pyYmodemSendDragBtn && (pyYmodemSendDragBtn.disabled = false);
- }
- }
-
- function getOfflineCode() {
- const flmFile = config.flmFile;
- const pythonSwdSpeed = swdClockSpeedMap[config.swdClockSpeed] || '10000000';
-
- // 生成多文件烧录代码
- 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()`;
-
- // 按算法分组文件
- const algorithmGroups = {};
- config.files.forEach(file => {
- if (file.algorithm && file.fileName && file.address) {
- if (!algorithmGroups[file.algorithm]) {
- algorithmGroups[file.algorithm] = [];
- }
- algorithmGroups[file.algorithm].push(file);
- }
- });
-
- // 为每个算法生成加载和烧录代码
- Object.keys(algorithmGroups).forEach((algorithm, index) => {
- const files = algorithmGroups[algorithm];
- if (files.length > 0) {
- // 加载算法
- offlineCode += `\n# 加载 ${algorithm} 下载算法文件\nresult = ReadFlm.load("FLM/${algorithm}", ${config.address1}, ${config.address2})\nif result != 0:\n return`;
-
- // 烧录该算法下的所有文件
- files.forEach(file => {
- offlineCode += `\n\n# 烧写 ${file.fileName}\nresult = load.bin("${file.fileName}", ${file.address})\nif result != 0:\n return`;
- });
- }
- });
-
- offlineCode += `\n\n# 蜂鸣器响一声,表示烧写完成\nbuzzer.enable()\nbuzzer.high()\ntime.sleep_ms(500)\nbuzzer.low()\ntime.sleep_ms(500)`;
-
- return offlineCode;
- }
- function getDragCode() {
- const flmFile = config.flmFile;
- const pythonSwdSpeed = swdClockSpeedMap[config.swdClockSpeed] || '10000000';
- return `import FLMConfig\ncmd.set_swd_clock(${pythonSwdSpeed})\nReadFlm = FLMConfig.ReadFlm()\nres1 = ReadFlm.load(\"FLM/${flmFile}\",${config.address1},${config.address2})`;
- }
- // 删除旧的pyYmodemSendBtn事件绑定
-
- // 统一事件绑定,确保使用正确的脚本内容
- const pyYmodemSendOfflineBtn = document.getElementById('pyYmodemSendOfflineBtn');
- const pyYmodemSendDragBtn = document.getElementById('pyYmodemSendDragBtn');
- if (pyYmodemSendOfflineBtn) {
- pyYmodemSendOfflineBtn.addEventListener('click', async () => {
- await sendPythonYmodem(getOfflineCode(), 'Python/offline_download.py');
- });
- }
- if (pyYmodemSendDragBtn) {
- pyYmodemSendDragBtn.addEventListener('click', async () => {
- await sendPythonYmodem(getDragCode(), 'Python/drag_download.py');
- });
- }
-
- window.updateCodePreview();
- }
- // ... 现有代码 ...
- // ... 只展示相关修改 ...
- // 事件绑定移到函数定义之后
- // 删除重复的全局getOfflineCode函数定义
- function getDragCode() {
- const customFlmInput = document.getElementById('customFlm');
- const address1Input = document.getElementById('address1');
- const address2Input = document.getElementById('address2');
- const swdClockSpeedSelect = document.getElementById('swdClockSpeed');
- const flmFile = customFlmInput ? customFlmInput.value || 'custom_flm.FLM.o' : 'custom_flm.FLM.o';
- const address1 = address1Input ? address1Input.value || '0X08000000' : '0X08000000';
- const address2 = address2Input ? address2Input.value || '0x20000000' : '0x20000000';
- const swdClockSpeedMap = { '10M': '10000000', '5M': '5000000', '2M': '2000000', '1M': '1000000', '500K': '500000', '200K': '200000', '100K': '100000', '50K': '50000', '20K': '20000', '10K': '10000', '5K': '5000' };
- const pythonSwdSpeed = swdClockSpeedMap[swdClockSpeedSelect ? swdClockSpeedSelect.value : '10M'] || '10000000';
- return `import FLMConfig\ncmd.set_swd_clock(${pythonSwdSpeed})\nReadFlm = FLMConfig.ReadFlm()\nres1 = ReadFlm.load(\"FLM/${flmFile}\",${address1},${address2})`;
- }
- // 删除重复的sendPythonYmodem函数定义
- // 删除重复的事件绑定,统一在setupPythonScriptPanel中处理
- // FLM .o文件 YMODEM发送
- async function handleFlmYmodemSend() {
- const flmYmodemSendBtn = document.getElementById('flmYmodemSendBtn');
- const flmYmodemProgress = document.getElementById('flmYmodemProgress');
- const log = document.getElementById('log');
- // 依赖 convertedBlob, flmFileName, isSerialConnected
- if (!window.convertedBlob) {
- if (log) log.textContent += '\n请先生成.o文件';
- return;
- }
- if (!window.microLinkTerminal || !window.microLinkTerminal.isConnected || !window.microLinkTerminal.port) {
- if (log) log.textContent += '\n请先连接串口';
- return;
- }
- flmYmodemSendBtn.disabled = true;
- flmYmodemProgress.style.display = '';
- flmYmodemProgress.value = 0;
- if (log) log.textContent += '\n准备发送...';
- let wasConnected = false;
- if (window.microLinkTerminal) {
- if (window.microLinkTerminal.reader) {
- try { window.microLinkTerminal.reader.cancel(); } catch(e){}
- try { window.microLinkTerminal.reader.releaseLock(); } catch(e){}
- window.microLinkTerminal.reader = null;
- }
- wasConnected = window.microLinkTerminal.isConnected;
- window.microLinkTerminal.isConnected = false;
- await new Promise(r => setTimeout(r, 300));
- }
- try {
- const arrayBuffer = await window.convertedBlob.arrayBuffer();
- const uint8Array = new Uint8Array(arrayBuffer);
- // 修改点:加上 FLM/ 前缀
- const fileName = 'FLM/' + (window.flmFileName ? (window.flmFileName + '.FLM.o') : 'firmware.FLM.o');
- const port = window.microLinkTerminal && window.microLinkTerminal.port;
- let ok = await window.sendFileViaYmodem(
- port,
- uint8Array,
- fileName,
- uint8Array.length,
- progress => { flmYmodemProgress.value = progress; },
- msg => { if (log) log.textContent += '\n' + msg; }
- );
- if (ok) {
- if (log) log.textContent += '\n✅ 文件发送完成';
- } else {
- if (log) log.textContent += '\n❌ 发送失败';
- }
- } catch (e) {
- if (log) log.textContent += '\n❌ 发送失败: ' + e.message;
- } finally {
- if (window.microLinkTerminal) {
- window.microLinkTerminal.isConnected = wasConnected;
- if (wasConnected && typeof window.microLinkTerminal.startReading === 'function') {
- window.microLinkTerminal.startReading();
- }
- }
- flmYmodemSendBtn.disabled = false;
- flmYmodemProgress.style.display = 'none';
- }
- }
- // Python脚本 YMODEM发送
- async function handlePyYmodemSend(type) {
- const pyYmodemSendOfflineBtn = document.getElementById('pyYmodemSendOfflineBtn');
- const pyYmodemSendDragBtn = document.getElementById('pyYmodemSendDragBtn');
- const pyYmodemProgress = document.getElementById('pyYmodemProgress');
- const pyYmodemLog = document.getElementById('pyYmodemLog');
- function pyYlog(msg, color) {
- appendToTerminalOutput(`<div class='log-prefix-python'>[PYTHON] ${msg}</div>`);
- }
- function pyYlogClear() {
- // 不清空主终端
- }
- if (!window.microLinkTerminal || !window.microLinkTerminal.isConnected || !window.microLinkTerminal.port) {
- pyYlog('请先连接串口', '#f66'); return;
- }
- pyYmodemSendOfflineBtn && (pyYmodemSendOfflineBtn.disabled = true);
- pyYmodemSendDragBtn && (pyYmodemSendDragBtn.disabled = true);
- pyYmodemProgress && (pyYmodemProgress.value = 0);
- pyYmodemProgress && (pyYmodemProgress.style.display = '');
- pyYlogClear();
- pyYlog('准备发送...', '#0ff');
- let wasConnected = false;
- if (window.microLinkTerminal) {
- if (window.microLinkTerminal.reader) {
- try { window.microLinkTerminal.reader.cancel(); } catch(e){}
- try { window.microLinkTerminal.reader.releaseLock(); } catch(e){}
- window.microLinkTerminal.reader = null;
- }
- wasConnected = window.microLinkTerminal.isConnected;
- window.microLinkTerminal.isConnected = false;
- await new Promise(r => setTimeout(r, 300));
- }
- try {
- let code = '';
- let fileName = '';
- if (type === 'offline') {
- code = window.getOfflineCode ? window.getOfflineCode() : '';
- fileName = 'Python/offline_download.py';
- } else if (type === 'drag') {
- code = window.getDragCode ? window.getDragCode() : '';
- fileName = 'Python/drag_download.py';
- } else {
- pyYlog('只允许发送离线下载脚本或拖拽下载脚本', '#f66');
- return;
- }
- const uint8Array = new TextEncoder().encode(code);
- const port = window.microLinkTerminal && window.microLinkTerminal.port;
- let ok = await window.sendFileViaYmodem(
- port,
- uint8Array,
- fileName,
- uint8Array.length,
- msg => pyYlog(msg)
- );
- if (ok) {
- pyYlog('✅ 发送完成', '#0f0');
- } else {
- pyYlog('❌ 发送失败', '#f66');
- }
- } catch (e) {
- pyYlog('❌ 发送失败: ' + e.message, '#f66');
- if (e && e.stack) pyYlog('错误堆栈: ' + e.stack, '#f66');
- } finally {
- if (window.microLinkTerminal) {
- window.microLinkTerminal.isConnected = wasConnected;
- if (wasConnected && typeof window.microLinkTerminal.startReading === 'function') {
- window.microLinkTerminal.startReading();
- }
- }
- pyYmodemSendOfflineBtn && (pyYmodemSendOfflineBtn.disabled = false);
- pyYmodemSendDragBtn && (pyYmodemSendDragBtn.disabled = false);
- pyYmodemProgress && (pyYmodemProgress.style.display = 'none');
- }
- }
- // ... existing code ...
- function setupVarAnalysisPanel() {
- // 只绑定一次
- if (window._varPanelInited) return;
- window._varPanelInited = true;
- const fileInput = document.getElementById('axfFile');
- const analyzeBtn = document.getElementById('analyzeBtn');
- if (!fileInput || !analyzeBtn) return;
- fileInput.addEventListener('change', function(e) {
- if (this.files.length > 0) {
- analyzeBtn.disabled = false;
- const fileInfo = document.getElementById('fileInfo');
- if (fileInfo) fileInfo.classList.add('d-none');
- } else {
- analyzeBtn.disabled = true;
- }
- });
- }
- // ... existing code ...
- // ========== 变量分析tab实时曲线功能 ==========
- let chartData = [];
- let chartInstance = null;
- let chartDrawEnabled = false;
- // 将chartDrawEnabled挂载到全局,供数据处理函数使用
- window.chartDrawEnabled = chartDrawEnabled;
- function setupRealtimeChart() {
- const chartDom = document.getElementById('realtimeChart');
- if (!chartDom) return;
- if (!window.echarts) return;
-
- // 初始化图表数据
- if (!window.chartData) {
- window.chartData = [];
- }
-
- chartInstance = echarts.init(chartDom);
-
- // 延迟初始化多变量图表管理器,确保DOM元素已创建
- setTimeout(() => {
- window.multiChartManager = new MultiChartManager();
- console.log('多变量图表管理器初始化完成');
- }, 200);
-
-
- // 初始化后强制resize,确保图表尺寸正确
- setTimeout(() => {
- if (chartInstance) {
- chartInstance.resize();
- }
- }, 100);
-
-
- // 采用test.html的ECharts配置
- chartInstance.setOption({
- xAxis: {
- type: 'value',
- name: '时间 (秒)',
- nameLocation: 'middle',
- nameGap: 30
- },
- yAxis: {
- type: 'value',
- scale: true,
- name: '数值',
- nameLocation: 'middle',
- nameGap: 40
- },
- series: [{
- type: 'line',
- data: [],
- smooth: true,
- symbol: 'none',
- lineStyle: { width: 2, color: '#3498db' },
- name: '实时数据'
- }],
- grid: {
- left: 80,
- right: 40,
- top: 50,
- bottom: 80,
- containLabel: true
- },
- animation: false,
- tooltip: {
- trigger: 'axis',
- formatter: function(params) {
- const data = params[0];
- return `时间: ${data.value[0].toFixed(2)}s<br/>数值: ${data.value[1]}`;
- }
- }
- });
- document.getElementById('clearChartBtn').onclick = function() {
- window.chartData = [];
- chartTimeData = []; // 清除时间数据
- chartDataBuffer = []; // 清除缓冲区
- dataIntegrityErrors = 0; // 重置错误计数
- expectedDataPattern = null; // 重置数据模式
- lastProcessedTime = 0; // 重置时间戳
-
- // 停止Worker
- if (chartDataWorker) {
- chartDataWorker.postMessage({ type: 'stop' });
- }
- if (chartRenderWorker) {
- chartRenderWorker.postMessage({ type: 'stop' });
- }
-
- // 清空多变量图表
- if (window.multiChartManager) {
- window.multiChartManager.clearAllCharts();
- }
-
- // 清空图表数据,不调用updateRealtimeChart避免覆盖时间轴逻辑
- if (chartInstance) {
- chartInstance.setOption({
- series: [{
- data: []
- }]
- });
- }
- // 重置开始绘制按钮状态
- const startBtn = document.getElementById('startBtn');
- if (startBtn) {
- startBtn.textContent = '开始绘制';
- startBtn.className = 'btn btn-success';
- }
- chartDrawEnabled = false;
- window.chartDrawEnabled = chartDrawEnabled;
- console.log('[曲线模式] 已清除数据并停止绘制');
- console.log('[曲线缓冲] 已清除缓冲区');
- console.log('[数据完整性] 已重置错误计数器和数据模式');
- console.log('[Worker] 已停止数据处理Worker');
- console.log('[数据帧解析] 已清空数据帧缓冲区');
-
- };
- // 开始绘制按钮
- const startBtn = document.getElementById('startChartBtn');
- if (startBtn) {
- startBtn.onclick = function() {
- if (!chartDrawEnabled) {
- // 开始绘制
- chartDrawEnabled = true;
- window.chartDrawEnabled = chartDrawEnabled;
- startBtn.textContent = '暂停绘制';
- startBtn.className = 'btn btn-warning';
-
- // 自动开启HEX模式
- const hexModeCheckbox = document.getElementById('hexMode');
- if (hexModeCheckbox && !hexModeCheckbox.checked) {
- hexModeCheckbox.checked = true;
- hexModeCheckbox.dispatchEvent(new Event('change'));
- }
-
- // 在控制台显示曲线模式已启动
- console.log('[曲线模式] 已启动 - 串口数据将直接用于曲线绘制,不显示在主监控');
- console.log('[曲线模式] 请确保设备发送4字节对齐的HEX数据');
-
- // 清除缓冲区,准备接收新数据
- chartDataBuffer = [];
- chartTimeData = []; // 清除时间数据
- dataIntegrityErrors = 0; // 重置错误计数
- expectedDataPattern = null; // 重置数据模式
- lastProcessedTime = 0; // 重置时间戳
-
-
- // 初始化Worker
- initChartWorkers();
-
- // 启动Worker
- if (chartDataWorker) {
- chartDataWorker.postMessage({ type: 'start' });
- }
- if (chartRenderWorker) {
- chartRenderWorker.postMessage({ type: 'start' });
- }
-
- console.log('[曲线缓冲] 已清除缓冲区,准备接收新数据');
- console.log('[数据完整性] 已重置错误计数器和数据模式');
- console.log('[Worker] 已启动数据处理Worker');
-
- } else {
- // 暂停绘制
- chartDrawEnabled = false;
- window.chartDrawEnabled = chartDrawEnabled;
- startBtn.textContent = '继续绘制';
- startBtn.className = 'btn btn-success';
-
- // 停止Worker
- if (chartDataWorker) {
- chartDataWorker.postMessage({ type: 'stop' });
- }
- if (chartRenderWorker) {
- chartRenderWorker.postMessage({ type: 'stop' });
- }
-
- console.log('[曲线模式] 已暂停 - 恢复正常监控显示');
- console.log('[Worker] 已停止数据处理Worker');
- }
- };
- }
-
- // 终止绘制按钮逻辑
- const stopChartBtn = document.getElementById('stopChartBtn');
- if (stopChartBtn) {
- stopChartBtn.onclick = function() {
- console.log('[终止绘制] 按钮被点击');
-
- // 1. 清空曲线数据
- window.chartData = [];
- chartTimeData = []; // 清除时间数据
- chartDataBuffer = []; // 清除缓冲区
- dataIntegrityErrors = 0; // 重置错误计数
- expectedDataPattern = null; // 重置数据模式
- lastProcessedTime = 0; // 重置时间戳
-
- // 2. 停止曲线绘制
- if (window.chartDrawEnabled) {
- window.chartDrawEnabled = false;
- chartDrawEnabled = false;
-
- // 更新按钮状态
- const startBtn = document.getElementById('startChartBtn');
- if (startBtn) {
- startBtn.textContent = '开始绘制';
- startBtn.className = 'btn btn-success';
- }
-
- // 停止Worker
- if (window.chartDataWorker) {
- window.chartDataWorker.postMessage({ type: 'stop' });
- }
- if (window.chartRenderWorker) {
- window.chartRenderWorker.postMessage({ type: 'stop' });
- }
-
- console.log('[终止绘制] 曲线绘制已停止');
- }
-
- // 3. 清空多变量图表
- if (window.multiChartManager) {
- window.multiChartManager.clearAllCharts();
- }
-
- // 4. 清空图表显示
- if (chartInstance) {
- chartInstance.setOption({
- series: [{
- data: []
- }]
- });
- }
-
- // 5. 发送终止绘制命令(如果串口已连接)
- const stopCommand = 'cmd.read_ram(0x20000000,1,0)';
- if (window.microLinkTerminal && window.microLinkTerminal.isConnected) {
- window.microLinkTerminal.sendCommand(stopCommand);
- console.log('[终止绘制] 已发送停止命令:', stopCommand);
- } else {
- console.log('[终止绘制] 串口未连接,跳过发送命令');
- }
-
- // 6. 关闭监听框的HEX模式
- const hexModeCheckbox = document.getElementById('hexMode');
- if (hexModeCheckbox && hexModeCheckbox.checked) {
- hexModeCheckbox.checked = false;
- hexModeCheckbox.dispatchEvent(new Event('change'));
- console.log('[终止绘制] 已关闭HEX模式');
- }
-
- console.log('[终止绘制] 操作完成 - 已清空曲线并停止绘制');
- };
- }
- // 保证全局可用(每次都强制挂载,防止tab切换后失效)
- window.chartData = window.chartData || [];
- window.updateRealtimeChart = updateRealtimeChart;
- // 旧的handleRealtimeHexData已删除,现在使用Worker方式
- window.chartInstance = chartInstance;
- // 再次初始化终端区监听,防止tab切换后丢失
- if (!window._terminalHexChartSyncInited) {
- setupTerminalHexChartSync();
- window._terminalHexChartSyncInited = true;
- }
- }
- // 全局时间轴数据
- let chartTimeData = [];
- // 将chartTimeData挂载到全局,供多变量图表使用
- window.chartTimeData = chartTimeData;
- function updateRealtimeChart() {
- if (!chartInstance) return;
- // 确保有足够的数据点来显示X轴
- const data = window.chartData || [];
- const timeData = chartTimeData || [];
-
- // 如果时间数据不足,补充时间数据
- while (timeData.length < data.length) {
- const currentTime = new Date();
- timeData.push(currentTime.toLocaleTimeString('zh-CN', {
- hour12: false,
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- fractionalSecondDigits: 3
- }));
- }
-
- // 动态计算标签间隔,避免重叠
- const maxLabels = 15; // 最多显示15个时间标签
- const interval = Math.max(0, Math.floor(timeData.length / maxLabels));
-
- chartInstance.setOption({
- xAxis: {
- type: 'category',
- data: timeData,
- axisLabel: {
- show: true,
- interval: interval,
- rotate: 45, // 旋转标签避免重叠
- fontSize: 10,
- margin: 12,
- formatter: function(value) {
- // 只显示时:分:秒,不显示毫秒
- return value.split('.')[0];
- }
- },
- axisTick: {
- show: true,
- interval: interval
- },
- name: '时间',
- nameLocation: 'middle',
- nameGap: 30
- },
- yAxis: {
- type: 'value',
- scale: true,
- axisLabel: {
- show: true,
- fontSize: 10
- },
- name: '数值',
- nameLocation: 'middle',
- nameGap: 40
- },
- series: [{
- type: 'line',
- data: data,
- smooth: true,
- symbol: 'none',
- lineStyle: { width: 2, color: '#3498db' },
- name: '实时数据'
- }],
- tooltip: {
- trigger: 'axis',
- formatter: function(params) {
- const data = params[0];
- const timeStr = timeData[data.dataIndex] || '未知时间';
- return `时间: ${timeStr}<br/>数值: ${data.value}`;
- }
- }
- });
-
- // 强制重新计算布局
- setTimeout(() => {
- if (chartInstance) {
- chartInstance.resize();
- // 非全屏时也强制重新计算,模拟全屏的效果
- setTimeout(() => {
- chartInstance.resize();
- }, 50);
- }
- }, 100);
- }
- // 全局缓冲区用于处理分片数据
- let chartDataBuffer = [];
- let lastProcessedTime = 0;
- let dataIntegrityErrors = 0;
- let expectedDataPattern = null; // 用于检测数据模式
- let chartDataWorker = null; // 数据处理Worker
- let chartRenderWorker = null; // 渲染Worker
- // 初始化Worker
- function initChartWorkers() {
- if (chartDataWorker || chartRenderWorker) return;
-
- // 数据Worker脚本 - 采用test.html的简单策略
- const dataWorkerScript = `
- let dataBuffer = [];
- let isRunning = false;
- let lastReportTime = 0;
-
- onmessage = function(e) {
- switch (e.data.type) {
- case 'start':
- isRunning = true;
- dataBuffer = [];
- lastReportTime = performance.now();
- break;
-
- case 'stop':
- isRunning = false;
- break;
-
- case 'serial_data':
- if (isRunning) {
- // 使用传入的时间戳,完全采用test.html的方式
- e.data.values.forEach(val => {
- dataBuffer.push({ timestamp: e.data.timestamp, value: val });
- });
-
- // 每100个数据点或100ms发送一次(参考test.html)
- if (dataBuffer.length >= 100 || performance.now() - lastReportTime > 100) {
- postMessage({
- type: 'data_batch',
- data: dataBuffer.splice(0, dataBuffer.length)
- });
- lastReportTime = performance.now();
- }
- }
- break;
- }
- };
- `;
-
- // 渲染Worker脚本 - 完全采用test.html的逻辑
- const renderWorkerScript = `
- let isRunning = false;
- let renderRate = 60;
- let maxDataPoints = 10000;
- let dataBuffer = [];
- let lastRenderTime = 0;
- let renderCount = 0;
- let startTime = 0;
- let totalDataPoints = 0;
- let timeWindow = 5;
- let historyPosition = 1.0;
-
- function processData() {
- if (!isRunning) return;
-
- const now = performance.now();
- if (now - lastRenderTime >= 1000 / renderRate) {
- const chartData = prepareChartData();
- postMessage({
- type: 'render',
- data: chartData,
- stats: {
- renderRate: renderCount / ((now - startTime) / 1000),
- totalDataPoints: totalDataPoints,
- dataBufferSize: dataBuffer.length
- }
- });
-
- lastRenderTime = now;
- renderCount++;
- }
-
- if (isRunning) {
- setTimeout(processData, 1000 / renderRate);
- }
- }
-
- function prepareChartData() {
- if (dataBuffer.length === 0) return [];
-
- // 根据历史位置计算要显示的数据范围
- const totalPoints = dataBuffer.length;
- const startIndex = Math.floor(totalPoints * (1 - historyPosition));
- const endIndex = totalPoints;
-
- // 获取指定范围的数据
- const filteredData = dataBuffer.slice(startIndex, endIndex);
-
- // 返回数据,时间轴显示实际时间(秒)- 完全采用test.html的方式
- const result = filteredData.map(item => ({
- time: (item.timestamp - dataBuffer[0].timestamp) / 1000,
- value: item.value
- }));
-
- // 移除调试信息,时间轴已经正常工作
-
- return result;
- }
-
- function compressData() {
- // 限制数据点数量,避免内存占用过大
- if (dataBuffer.length > maxDataPoints) {
- const step = Math.ceil(dataBuffer.length / maxDataPoints);
- dataBuffer = dataBuffer.filter((_, index) => index % step === 0);
- }
- }
-
- function cleanupOldData() {
- const now = performance.now();
- // 保留最近60秒的数据,避免内存无限增长
- const cutoffTime = now - 60000;
- dataBuffer = dataBuffer.filter(item => item.timestamp >= cutoffTime);
- }
-
- onmessage = function(e) {
- switch (e.data.type) {
- case 'start':
- isRunning = true;
- renderRate = e.data.renderRate || 60;
- maxDataPoints = e.data.maxDataPoints || 10000;
- timeWindow = e.data.timeWindow || 5;
- historyPosition = e.data.historyPosition || 1.0;
- startTime = performance.now();
- renderCount = 0;
- totalDataPoints = 0;
- dataBuffer = [];
- lastRenderTime = 0;
- processData();
- break;
-
- case 'stop':
- isRunning = false;
- break;
-
- case 'data_batch':
- dataBuffer.push(...e.data.data);
- totalDataPoints += e.data.data.length;
- compressData();
- cleanupOldData();
- break;
-
- case 'config':
- historyPosition = e.data.historyPosition || historyPosition;
- break;
-
- case 'clear':
- dataBuffer = [];
- totalDataPoints = 0;
- break;
-
- case 'get_stats':
- postMessage({
- type: 'stats',
- stats: {
- renderRate: renderCount / ((performance.now() - startTime) / 1000),
- totalDataPoints: totalDataPoints,
- dataBufferSize: dataBuffer.length
- }
- });
- break;
- }
- };
- `;
-
- const dataWorkerBlob = new Blob([dataWorkerScript], { type: 'application/javascript' });
- const renderWorkerBlob = new Blob([renderWorkerScript], { type: 'application/javascript' });
-
- chartDataWorker = new Worker(URL.createObjectURL(dataWorkerBlob));
- chartRenderWorker = new Worker(URL.createObjectURL(renderWorkerBlob));
-
- // 设置Worker消息处理
- chartDataWorker.onmessage = (e) => {
- if (e.data.type === 'data_batch') {
- chartRenderWorker.postMessage({
- type: 'data_batch',
- data: e.data.data
- });
- }
- };
-
- chartRenderWorker.onmessage = (e) => {
- if (e.data.type === 'render') {
- updateChartFromWorker(e.data.data);
- }
- };
-
- console.log('[Worker] 图表Worker已初始化');
- }
- // 停止图表绘制
- function stopChartDrawing() {
- chartDrawEnabled = false;
- window.chartDrawEnabled = chartDrawEnabled;
-
- // 停止Worker
- if (chartDataWorker) {
- chartDataWorker.postMessage({ type: 'stop' });
- }
- if (chartRenderWorker) {
- chartRenderWorker.postMessage({ type: 'stop' });
- }
-
- // 重置性能统计
- performanceStats = {
- dataPointsReceived: 0,
- renderCount: 0,
- lastRenderTime: 0,
- averageRenderTime: 0
- };
-
- // 重置数据质量统计
- dataQualityStats = {
- totalBytesReceived: 0,
- validDataPoints: 0,
- errorCount: 0,
- recoveryCount: 0,
- lastReportTime: 0
- };
-
- // 更新按钮状态
- const startBtn = document.getElementById('startChartBtn');
- if (startBtn) {
- startBtn.textContent = '开始绘制';
- startBtn.className = 'btn btn-success';
- }
-
- console.log('[性能保护] 已自动停止图表绘制');
- }
- // 性能监控变量
- let performanceStats = {
- dataPointsReceived: 0,
- renderCount: 0,
- lastRenderTime: 0,
- averageRenderTime: 0
- };
- // 数据质量统计
- let dataQualityStats = {
- totalBytesReceived: 0,
- validDataPoints: 0,
- errorCount: 0,
- recoveryCount: 0,
- lastReportTime: 0
- };
- // 从Worker更新图表
- // 采用test.html的updateChart逻辑
- function updateChartFromWorker(data) {
- if (!chartInstance || !chartDrawEnabled) return;
-
- const startTime = performance.now();
-
- // 采用test.html的数据格式:直接使用time和value
- const chartData = data.map(d => [d.time, d.value]);
-
- // 更新性能统计
- performanceStats.dataPointsReceived += data.length;
- performanceStats.renderCount++;
- const renderTime = performance.now() - startTime;
- performanceStats.averageRenderTime = (performanceStats.averageRenderTime * (performanceStats.renderCount - 1) + renderTime) / performanceStats.renderCount;
-
- // 每10次渲染输出一次性能统计
- //if (performanceStats.renderCount % 10 === 0) {
- //console.log(`[性能统计] 数据点: ${performanceStats.dataPointsReceived}, 渲染次数: ${performanceStats.renderCount}, 平均渲染时间: ${performanceStats.averageRenderTime.toFixed(2)}ms`);
- //}
-
- // 性能保护:如果渲染时间过长,自动停止
- if (renderTime > 100) {
- console.warn(`[性能警告] 渲染时间过长: ${renderTime.toFixed(2)}ms,自动停止绘制`);
- stopChartDrawing();
- return;
- }
-
- // 自适应性能调节
- if (renderTime > 50 && performanceStats.renderCount > 20) {
- console.warn(`[性能调节] 渲染时间较长: ${renderTime.toFixed(2)}ms,建议降低采样率或关闭完整性检查`);
- }
-
- // 完全采用test.html的updateChart逻辑
- const chartDataArray = data.map(d => [d.time, d.value]);
-
- const option = {
- series: [{
- data: chartDataArray
- }]
- };
-
- // 采用新工程的x轴配置方式,不设置min/max,让ECharts自动计算合适的范围
- if (data.length > 0) {
- option.xAxis = {
- type: 'value',
- name: '时间 (秒)',
- nameLocation: 'middle',
- nameGap: 30
- };
- }
-
- chartInstance.setOption(option);
- }
- function handleRealtimeHexDataChunked(data) {
- if (!chartDrawEnabled) return;
- if (!(data instanceof Uint8Array)) return;
-
- const currentTime = Date.now();
-
- // 更新数据质量统计
- dataQualityStats.totalBytesReceived += data.length;
-
- // 采用test.html的数组方式处理数据
- // 将Uint8Array转换为数组,便于使用push和splice
- if (!Array.isArray(chartDataBuffer)) {
- chartDataBuffer = Array.from(chartDataBuffer);
- }
-
- // 采用test.html的数据转换方式:逐个字节转换
- const values = [];
- for (let i = 0; i < data.length; i++) {
- values.push(data[i]);
- }
-
- // 将新数据添加到缓冲区
- chartDataBuffer.push(...values);
-
- // 调试输出:显示拼接后的数据
- if (chartDataBuffer.length > 0) {
- const firstBytes = chartDataBuffer.slice(0, Math.min(8, chartDataBuffer.length))
- .map(b => b.toString(16).padStart(2, '0')).join(' ');
- console.log(`[数据拼接] 缓冲区大小: ${chartDataBuffer.length}, 前8字节: ${firstBytes}`);
-
- // 简化调试输出,只在调试模式下显示详细信息
- const debugCheckbox = document.getElementById('debugMode');
- const enableDebug = debugCheckbox && debugCheckbox.checked;
-
- if (enableDebug) {
- // 如果剩余字节不是4的倍数,记录警告
- if (chartDataBuffer.length % 4 !== 0) {
- console.warn(`[边界警告] 缓冲区大小 ${chartDataBuffer.length} 不是4的倍数`);
- }
- }
- }
-
- console.log(`[曲线缓冲] 缓冲区大小: ${chartDataBuffer.length} 字节`);
-
- // 采用test.html的即时处理策略:每次接收到数据就立即处理
- // 处理完整的4字节组
- while (chartDataBuffer.length >= 4) {
- // 采用test.html的简单策略:不做任何数据验证,直接处理所有数据
- const value = (chartDataBuffer[0] | (chartDataBuffer[1]<<8) | (chartDataBuffer[2]<<16) | (chartDataBuffer[3]<<24)) >>> 0;
- const seg = [chartDataBuffer[0], chartDataBuffer[1], chartDataBuffer[2], chartDataBuffer[3]];
-
- // 发送数据到Worker
- if (chartDataWorker) {
- chartDataWorker.postMessage({
- type: 'serial_data',
- values: [value]
- });
- }
-
- // 在控制台打印详细信息
- console.log(`[曲线解析] ✅ 4字节: ${seg.map(x=>x.toString(16).padStart(2,'0')).join(' ')} -> 小端解析: 0x${value.toString(16).padStart(8,'0')} (${value})`);
-
- // 更新最后处理时间
- lastProcessedTime = currentTime;
-
- // 重置错误计数
- dataIntegrityErrors = 0;
-
- // 更新统计
- dataQualityStats.validDataPoints++;
-
- // 移除已处理的4字节(参考test.html的splice方法)
- chartDataBuffer.splice(0, 4);
- }
-
- // 移除已处理的4字节(参考test.html的splice方法)
- chartDataBuffer.splice(0, 4);
- }
-
- // 检查缓冲区是否积压过多(可能数据丢失)
- if (chartDataBuffer.length > 20) {
- console.warn(`[曲线警告] 缓冲区积压过多 (${chartDataBuffer.length} 字节),可能数据丢失`);
- chartDataBuffer = [];
- dataIntegrityErrors++;
- }
-
- // 定期输出数据质量统计
- const now = performance.now();
- if (now - dataQualityStats.lastReportTime > 5000) { // 每5秒输出一次
- const errorRate = dataQualityStats.totalBytesReceived > 0 ?
- (dataQualityStats.errorCount / dataQualityStats.totalBytesReceived * 100).toFixed(2) : '0.00';
- const recoveryRate = dataQualityStats.errorCount > 0 ?
- (dataQualityStats.recoveryCount / dataQualityStats.errorCount * 100).toFixed(2) : '0.00';
-
- console.log(`[数据质量统计] 总字节: ${dataQualityStats.totalBytesReceived}, 有效数据点: ${dataQualityStats.validDataPoints}, 错误: ${dataQualityStats.errorCount}, 恢复: ${dataQualityStats.recoveryCount}, 错误率: ${errorRate}%, 恢复率: ${recoveryRate}%`);
-
- dataQualityStats.lastReportTime = now;
- }
-
- console.log(`[曲线处理] 完成处理,剩余缓冲区: ${chartDataBuffer.length} 字节,错误次数: ${dataIntegrityErrors}`);
- // 智能数据恢复函数
- function attemptSmartRecovery() {
- if (chartDataBuffer.length < 8) {
- return false; // 缓冲区数据不足
- }
-
- console.log(`[数据恢复] 尝试智能恢复,缓冲区大小: ${chartDataBuffer.length}`);
-
- // 尝试不同的偏移量
- for (let offset = 1; offset <= 4 && chartDataBuffer.length >= 4 + offset; offset++) {
- const testBytes = [
- chartDataBuffer[offset],
- chartDataBuffer[offset+1],
- chartDataBuffer[offset+2],
- chartDataBuffer[offset+3]
- ];
- const testValue = (testBytes[0] | (testBytes[1]<<8) | (testBytes[2]<<16) | (testBytes[3]<<24)) >>> 0;
-
- // 检查这个偏移量是否产生合理的数据
- if (testValue > 0 && testValue <= 1000000) {
- console.log(`[数据恢复] 找到有效偏移量: ${offset},新值: ${testValue}`);
- chartDataBuffer = chartDataBuffer.slice(offset);
- dataQualityStats.recoveryCount++;
- return true;
- }
- }
-
- // 如果找不到有效偏移量,丢弃前4字节
- console.log(`[数据恢复] 未找到有效偏移量,丢弃前4字节`);
- chartDataBuffer = chartDataBuffer.slice(4);
- return false;
- }
- // 数据完整性检查函数
- function checkDataIntegrity(bytes, value) {
- // 检查1: 数值合理性(不能为0或过大)
- if (value === 0) {
- console.warn(`[完整性检查] 数值为0,可能数据丢失`);
- return false;
- }
-
- if (value > 0x7FFFFFFF) {
- console.warn(`[完整性检查] 数值过大 (${value}),可能数据错位`);
- return false;
- }
-
- // 检查2: 时间间隔合理性(放宽限制)
- const currentTime = Date.now();
- if (lastProcessedTime > 0 && (currentTime - lastProcessedTime) > 30000) { // 改为30秒
- console.warn(`[完整性检查] 数据间隔过长 (${currentTime - lastProcessedTime}ms),可能数据丢失`);
- return false;
- }
-
- // 检查3: 数据模式一致性(只在有足够数据且模式稳定时检查)
- if (expectedDataPattern !== null && window.chartData && window.chartData.length > 10) {
- const patternMatch = checkDataPattern(bytes);
- if (!patternMatch) {
- console.warn(`[完整性检查] 数据模式不匹配,可能数据错位`);
- return false;
- }
- }
-
- // 建立数据模式(前几个数据点)
- if (window.chartData && window.chartData.length < 3) {
- establishDataPattern(bytes);
- }
-
- return true;
- }
- // 建立数据模式
- function establishDataPattern(bytes) {
- if (expectedDataPattern === null) {
- expectedDataPattern = {
- firstByte: bytes[0],
- secondByte: bytes[1],
- pattern: []
- };
- console.log(`[模式建立] 建立数据模式: 首字节=${bytes[0].toString(16).padStart(2,'0')}, 次字节=${bytes[1].toString(16).padStart(2,'0')}`);
- }
- }
- // 检查数据模式
- function checkDataPattern(bytes) {
- if (expectedDataPattern === null) return true;
-
- // 放宽模式检查:只要前两个字节不是完全相同的固定值就认为有效
- // 这样可以适应数据变化的情况
- if (bytes[0] !== bytes[1]) {
- return true;
- }
-
- // 如果前两个字节相同,可能是固定模式,需要进一步检查
- if (bytes[0] === expectedDataPattern.firstByte && bytes[1] === expectedDataPattern.secondByte) {
- return true;
- }
-
- return false;
- }
- // 尝试数据恢复
- function attemptDataRecovery() {
- console.log(`[数据恢复] 尝试在缓冲区中寻找有效数据边界...`);
-
- // 寻找可能的4字节边界
- for (let i = 1; i < chartDataBuffer.length - 3; i++) {
- const testBytes = [chartDataBuffer[i], chartDataBuffer[i+1], chartDataBuffer[i+2], chartDataBuffer[i+3]];
- const testValue = (testBytes[0] | (testBytes[1]<<8) | (testBytes[2]<<16) | (testBytes[3]<<24)) >>> 0;
-
- // 检查这个位置是否可能是有效数据
- if (testValue > 0 && testValue <= 0x7FFFFFFF) {
- console.log(`[数据恢复] 在位置 ${i} 找到可能的有效数据边界`);
- chartDataBuffer = chartDataBuffer.slice(i);
- return true;
- }
- }
-
- return false;
- }
- // 旧的handleRealtimeHexData函数已删除,现在使用Worker方式处理数据
- // ... existing code ...
- function setupTerminalHexChartSync() {
- // 这个函数现在不再需要,因为数据直接在handleReceivedData中处理
- // 保留函数以避免调用错误,但不执行任何操作
- console.log('[曲线同步] 已禁用终端监控同步,数据直接通过串口处理');
- }
- // ... existing code ...
- // ... existing code ...
- // 全屏样式
- (function(){
- const style = document.createElement('style');
- 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}`;
- document.head.appendChild(style);
- })();
- // ... existing code ...
- // ... existing code ...
- // ... existing code ...
- // ... existing code ...
- // 页面加载时初始化曲线图
- if (document.readyState === 'complete' || document.readyState === 'interactive') {
- setTimeout(setupRealtimeChart, 300);
- } else {
- document.addEventListener('DOMContentLoaded', setupRealtimeChart);
- }
- // 立即绑定终止绘制按钮事件,确保按钮可用
- function bindStopChartButton() {
- const stopChartBtn = document.getElementById('stopChartBtn');
- if (stopChartBtn) {
- stopChartBtn.onclick = function() {
- console.log('[终止绘制] 按钮被点击');
-
- // 1. 清空曲线数据
- window.chartData = [];
- window.chartTimeData = []; // 清除时间数据
- window.chartDataBuffer = []; // 清除缓冲区
-
- // 2. 停止曲线绘制
- if (window.chartDrawEnabled) {
- window.chartDrawEnabled = false;
-
- // 更新按钮状态
- const startBtn = document.getElementById('startChartBtn');
- if (startBtn) {
- startBtn.textContent = '开始绘制';
- startBtn.className = 'btn btn-success';
- }
-
- // 停止Worker
- if (window.chartDataWorker) {
- window.chartDataWorker.postMessage({ type: 'stop' });
- }
- if (window.chartRenderWorker) {
- window.chartRenderWorker.postMessage({ type: 'stop' });
- }
-
- console.log('[终止绘制] 曲线绘制已停止');
- }
-
- // 3. 清空多变量图表
- if (window.multiChartManager) {
- window.multiChartManager.clearAllCharts();
- }
-
- // 4. 清空图表显示
- if (window.chartInstance) {
- window.chartInstance.setOption({
- series: [{
- data: []
- }]
- });
- }
-
- // 5. 发送终止绘制命令(如果串口已连接)
- const stopCommand = 'cmd.read_ram(0x20000000,1,0)';
- if (window.microLinkTerminal && window.microLinkTerminal.isConnected) {
- window.microLinkTerminal.sendCommand(stopCommand);
- console.log('[终止绘制] 已发送停止命令:', stopCommand);
- } else {
- console.log('[终止绘制] 串口未连接,跳过发送命令');
- }
-
- // 6. 关闭监听框的HEX模式
- const hexModeCheckbox = document.getElementById('hexMode');
- if (hexModeCheckbox && hexModeCheckbox.checked) {
- hexModeCheckbox.checked = false;
- hexModeCheckbox.dispatchEvent(new Event('change'));
- console.log('[终止绘制] 已关闭HEX模式');
- }
-
- console.log('[终止绘制] 操作完成 - 已清空曲线并停止绘制');
- };
- console.log('[终止绘制] 按钮事件已绑定');
- } else {
- console.log('[终止绘制] 按钮未找到,将在setupRealtimeChart中绑定');
- }
- }
- // 立即尝试绑定按钮
- bindStopChartButton();
- // 如果DOM还没准备好,等待后再次尝试
- if (document.readyState !== 'complete') {
- document.addEventListener('DOMContentLoaded', bindStopChartButton);
- }
- // 切换到变量分析tab时也初始化曲线图,防止切换后全局变量丢失
- function setupVarPanelRealtimeChartInit() {
- setTimeout(setupRealtimeChart, 200);
- }
- const varPanelBtn = document.querySelector('.sidebar-btn[data-panel="varPanel"]');
- if (varPanelBtn) {
- varPanelBtn.addEventListener('click', setupVarPanelRealtimeChartInit);
- }
- // ... existing code ...
- // ... existing code ...
- // 删除旧的串口发送区逻辑,因为HTML结构已经改变
- // ... existing code ...
- // ... existing code ...
- // 删除重复的开始绘制按钮逻辑,已在setupRealtimeChart中处理
- // ... existing code ...
- // 获取主终端输出区域
- function appendToTerminalOutput(html) {
- const terminalOutput = document.getElementById('terminalOutput');
- if (terminalOutput) {
- terminalOutput.insertAdjacentHTML('beforeend', html);
- terminalOutput.scrollTop = terminalOutput.scrollHeight;
- }
- }
- // 删除重复的全局日志函数定义
- // test.html的简化数据处理逻辑
- function parseSerialData(buffer) {
- const view = new Uint8Array(buffer);
- const values = [];
- // 每个字节作为一个独立的十进制值
- for (let i = 0; i < view.length; i++) {
- values.push(view[i]);
- }
- return values;
- }
- function processSerialBuffer() {
- // 如果缓冲区有足够的数据,按4字节一组处理
- while (chartDataBuffer.length >= 4) {
- const group = chartDataBuffer.splice(0, 4);
- console.log('处理4字节组:', group);
-
- // 将4字节组合成32位整数 (小端序 - 从后往前读)
- const value = group[0] | (group[1] << 8) | (group[2] << 16) | (group[3] << 24);
- console.log('32位整数值 (小端序):', value);
-
- // 发送数据到Worker - 添加时间戳,完全采用test.html的方式
- if (chartDataWorker) {
- const timestamp = performance.now();
- chartDataWorker.postMessage({
- type: 'serial_data',
- values: [value],
- timestamp: timestamp
- });
- }
- }
- }
- // 新的简化版本,支持AA和55数据帧头帧尾解析
- function handleRealtimeHexDataChunkedSimple(data) {
- if (!chartDrawEnabled) return;
- if (!(data instanceof Uint8Array)) return;
-
- // 将新数据添加到缓冲区
- if (!Array.isArray(chartDataBuffer)) {
- chartDataBuffer = [];
- }
- chartDataBuffer.push(...Array.from(data));
-
- // console.log('接收到串口数据块:', data.length, '字节, 缓冲区大小:', chartDataBuffer.length);
-
- // 查找并解析AA和55之间的数据帧
- parseDataFrames();
- }
- // 解析AA和55之间的数据帧
- function parseDataFrames() {
- const frameStart = 0xAA; // 帧头
- const frameEnd = 0x55; // 帧尾
-
- while (chartDataBuffer.length >= 2) {
- // 查找帧头AA
- const startIndex = chartDataBuffer.indexOf(frameStart);
- if (startIndex === -1) {
- // 没有找到帧头,清空缓冲区
- chartDataBuffer.splice(0, chartDataBuffer.length);
- return;
- }
-
- // 查找帧尾55(从帧头之后开始查找)
- const endIndex = chartDataBuffer.indexOf(frameEnd, startIndex + 1);
- if (endIndex === -1) {
- // 没有找到帧尾,保留从帧头开始的数据,等待更多数据
- chartDataBuffer.splice(0, startIndex);
- return;
- }
-
- // 提取帧头帧尾之间的数据
- const frameData = chartDataBuffer.slice(startIndex + 1, endIndex);
- // console.log('找到数据帧:', {
- // start: startIndex,
- // end: endIndex,
- // frameLength: frameData.length,
- // frameData: Array.from(frameData).map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase())
- // });
-
- // 处理提取的数据帧
- processDataFrame(frameData);
-
- // 移除已处理的数据(包括帧头、帧尾和中间数据)
- chartDataBuffer.splice(0, endIndex + 1);
- }
- }
- // 处理提取的数据帧
- function processDataFrame(frameData) {
- if (frameData.length === 0) return;
-
- // console.log('处理数据帧:', frameData.length, '字节:', Array.from(frameData).map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase()));
-
- // 检查是否有选中的变量
- if (!window.selectedVariables || window.selectedVariables.size === 0) {
- console.log('没有选中的变量,跳过数据处理');
- return;
- }
-
- // 获取选中的变量列表
- const selectedVars = Array.from(window.selectedVariables);
- const maxVars = Math.min(selectedVars.length, 9); // 最多9个变量
-
- console.log(`[数据处理] 开始处理数据帧:`, {
- '帧长度': frameData.length,
- '选中变量数量': selectedVars.length,
- '最大处理数量': maxVars,
- '选中变量': selectedVars,
- '数据内容': Array.from(frameData).map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase())
- });
-
- // 根据变量大小动态解析数据
- let dataIndex = 0;
- let frameIndex = 0;
-
- // 特殊处理:如果数据帧很小,调整处理策略
- if (frameData.length === 1) {
- console.log(`[数据处理] 检测到1字节数据帧,启用特殊处理模式`);
- // 对于1字节数据帧,强制第一个变量为1字节
- if (selectedVars.length > 0) {
- const varName = selectedVars[0];
- console.log(`[数据处理] 1字节模式:将变量 ${varName} 设置为1字节`);
- }
- }
-
- while (frameIndex < frameData.length && dataIndex < maxVars) {
- const varName = selectedVars[dataIndex];
-
- // 获取变量的字节大小 - 优先使用选择的变量实际大小
- let varSize = 4; // 默认4字节
-
- // 优先从变量信息中获取实际大小
- if (window.variableInfo && window.variableInfo[varName]) {
- varSize = window.variableInfo[varName].size || 4;
- console.log(`[数据处理] 使用变量 ${varName} 的实际大小: ${varSize}字节`);
- } else {
- // 如果无法获取变量大小,尝试从命令中推断
- const remainingBytes = frameData.length - frameIndex;
-
- // 特殊处理:如果数据帧只有1字节且是第一个变量,强制使用1字节
- if (frameData.length === 1 && dataIndex === 0) {
- varSize = 1;
- console.log(`[数据处理] 1字节数据帧模式:强制变量 ${varName} 为1字节`);
- } else if (remainingBytes >= 4) {
- // 数据充足,可以使用4字节
- varSize = 4;
- } else if (remainingBytes >= 2) {
- // 数据足够2字节
- varSize = 2;
- } else if (remainingBytes >= 1) {
- // 只有1字节数据,强制使用1字节
- varSize = 1;
- console.log(`[数据处理] 数据不足,将变量 ${varName} 调整为1字节大小`);
- } else {
- // 没有数据了
- console.log(`[数据处理] 没有剩余数据,跳过变量 ${varName}`);
- break;
- }
-
- console.log(`[数据处理] 智能推断变量 ${varName} 大小: ${varSize}字节 (剩余数据: ${remainingBytes}字节)`);
- }
-
- // 检查剩余数据是否足够(这个检查现在由智能推断逻辑处理)
- // 如果智能推断后仍然不足,记录警告但继续处理
- if (frameData.length - frameIndex < varSize) {
- console.warn(`[数据处理] 警告:变量 ${varName} 需要 ${varSize} 字节,但剩余 ${frameData.length - frameIndex} 字节,尝试调整大小`);
- // 强制调整为可用大小
- varSize = Math.min(varSize, frameData.length - frameIndex);
- if (varSize <= 0) {
- console.log(`[数据处理] 无法调整大小,跳过变量 ${varName}`);
- break;
- }
- console.log(`[数据处理] 已将变量 ${varName} 大小调整为 ${varSize} 字节`);
- }
-
- // 提取指定大小的数据
- const group = frameData.slice(frameIndex, frameIndex + varSize);
- frameIndex += varSize;
-
- // 根据大小解析数据 - 使用无符号整数(uint)处理
- let value;
- if (varSize === 1) {
- value = group[0]; // 直接使用无符号8位整数 (0-255)
- } else if (varSize === 2) {
- value = group[0] | (group[1] << 8); // 小端序16位无符号整数 (0-65535)
- } else if (varSize === 4) {
- value = group[0] | (group[1] << 8) | (group[2] << 16) | (group[3] << 24); // 小端序32位无符号整数 (0-4294967295)
- } else {
- // 对于其他大小,转换为无符号数值
- value = parseInt(Array.from(group).map(b => b.toString(16).padStart(2, '0')).join(''), 16);
- }
-
- console.log(`[数据处理] 变量 ${varName} (${varSize}字节):`, value, '原始字节:', group.map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase()));
-
- // 特殊显示1字节变量的信息
- if (varSize === 1) {
- console.log(`[数据处理] 1字节变量 ${varName} 处理完成:`, {
- '原始值': group[0],
- '无符号值': value,
- '范围': `${value >= 0 && value <= 255 ? '有效' : '超出范围'}`,
- '二进制': '0b' + group[0].toString(2).padStart(8, '0')
- });
- }
-
- // 发送数据到多变量图表管理器
- if (window.multiChartManager && window.chartDrawEnabled) {
- // 检查图表是否存在,如果不存在则跳过
- if (window.multiChartManager.hasChart(varName)) {
- console.log(`[数据处理] 更新图表 - 变量: ${varName}, 值: ${value}`);
- window.multiChartManager.updateChartData(varName, [value]);
- } else {
- console.log(`[数据处理] 跳过图表更新 - 变量 ${varName} 的图表不存在,尝试创建图表`);
- // 尝试创建图表
- window.multiChartManager.createChart(varName);
- // 延迟更新数据
- setTimeout(() => {
- if (window.multiChartManager.hasChart(varName)) {
- window.multiChartManager.updateChartData(varName, [value]);
- }
- }, 100);
- }
- }
-
- dataIndex++;
- }
-
- // 如果剩余数据不足4字节,记录日志
- if (frameData.length > 0) {
- // console.log('数据帧剩余不足4字节的数据:', frameData.length, '字节:', Array.from(frameData).map(b => '0x' + b.toString(16).padStart(2, '0').toUpperCase()));
- }
- }
- // 多变量图表管理器
- class MultiChartManager {
- constructor() {
- this.container = null;
- this.charts = new Map(); // 存储变量名到图表实例的映射
- this.chartData = new Map(); // 存储变量名到数据的映射
- this.chartTimeData = new Map(); // 存储变量名到时间数据的映射
- this.varTimestamps = new Map(); // 存储每个变量的时间戳序列
- this.defaultChart = null;
- this.samplingRate = 50; // 默认采样率50Hz
- this.timeDisplayRange = 10; // 默认时间显示范围10秒
- this.debugMode = false; // 调试模式开关,默认关闭
- this.lastUpdateTime = new Map(); // 每个变量的上次更新时间,用于节流绘制
- console.log(`[MultiChartManager] 初始化完成 - 默认采样率: ${this.samplingRate}Hz, 显示范围: ${this.timeDisplayRange}秒`);
-
- // 延迟初始化,等待DOM元素创建完成
- this.init();
- }
-
- init() {
- this.container = document.getElementById('multiChartContainer');
- this.defaultChart = document.getElementById('realtimeChart');
-
- if (!this.container) {
- console.error('多变量图表管理器容器未找到,将在100ms后重试');
- setTimeout(() => this.init(), 100);
- return;
- }
-
- console.log('多变量图表管理器已初始化');
- }
-
- /**
- * 为指定变量创建图表
- */
- createChart(varName) {
- console.log(`[MultiChartManager] 尝试创建图表: ${varName}`);
-
- if (this.charts.has(varName)) {
- console.log(`[MultiChartManager] 变量 ${varName} 的图表已存在,跳过创建`);
- return;
- }
-
- // 检查容器是否准备好
- if (!this.container) {
- console.log(`[MultiChartManager] 容器未准备好,延迟创建图表: ${varName}`);
- setTimeout(() => this.createChart(varName), 100);
- return;
- }
-
- console.log(`[MultiChartManager] 开始创建图表: ${varName}, 容器状态:`, !!this.container);
-
- // 隐藏默认图表
- if (this.defaultChart) {
- this.defaultChart.style.display = 'none';
- }
-
- // 创建图表容器
- const chartDiv = document.createElement('div');
- chartDiv.className = 'variable-chart';
- chartDiv.setAttribute('data-var', varName);
- chartDiv.innerHTML = `
- <div class="variable-chart-header">
- <div class="variable-chart-title">${varName}</div>
- <div class="variable-chart-controls">
- <button class="btn btn-sm btn-outline-info" onclick="window.multiChartManager.fullscreenChart('${varName}')" title="全屏显示">
- <span style="font-size:12px;">⛶</span>
- </button>
- </div>
- </div>
- <div class="variable-chart-content" style="width:100%;height:400px;"></div>
- `;
-
- // 调试:显示图表容器信息
- console.log(`[MultiChartManager] 创建图表容器 - 变量 ${varName}:`, {
- '容器类名': chartDiv.className,
- '容器属性': chartDiv.getAttribute('data-var'),
- '容器HTML': chartDiv.innerHTML.substring(0, 100) + '...',
- '容器高度': chartDiv.querySelector('.variable-chart-content').style.height
- });
-
- // 确保容器存在且可访问
- if (!this.container || !this.container.appendChild) {
- console.error(`[MultiChartManager] 容器不可用,无法创建图表 - 变量 ${varName}`);
- return;
- }
-
- this.container.appendChild(chartDiv);
-
- // 初始化ECharts实例
- const chartDom = chartDiv.querySelector('.variable-chart-content');
- let chartInstance;
-
- try {
- chartInstance = echarts.init(chartDom);
- console.log(`[MultiChartManager] ECharts实例创建成功 - 变量 ${varName}`);
- } catch (error) {
- console.error(`[MultiChartManager] ECharts实例创建失败 - 变量 ${varName}:`, error);
- // 移除失败的DOM元素
- chartDiv.remove();
- return;
- }
-
- // 设置图表配置,与默认图表保持一致
- try {
- chartInstance.setOption({
- xAxis: {
- type: 'value',
- name: '时间 (秒)',
- nameLocation: 'middle',
- nameGap: 30,
- min: 0,
- max: this.timeDisplayRange, // 使用动态监测时间
- axisLabel: {
- formatter: function(value) {
- return value.toFixed(1) + 's';
- }
- }
- },
- yAxis: {
- type: 'value',
- scale: true,
- name: '数值',
- nameLocation: 'middle',
- nameGap: 40,
- min: -130,
- max: 130,
- axisLabel: {
- formatter: function(value) {
- // 如果是1字节范围,显示整数
- if (value >= -128 && value <= 127) {
- return Math.round(value);
- }
- return value.toFixed(1);
- }
- }
- },
- series: [{
- type: 'line',
- data: [],
- smooth: true,
- symbol: 'none',
- lineStyle: { width: 2, color: '#3498db' },
- name: '实时数据'
- }],
- grid: {
- left: 80,
- right: 40,
- top: 50,
- bottom: 80,
- containLabel: true
- },
- animation: false,
- tooltip: {
- trigger: 'axis',
- formatter: function(params) {
- const data = params[0];
- const timeStr = params[0].axisValue || '未知时间';
- // 确保数值正确显示
- let valueStr;
- if (Array.isArray(data.value)) {
- // 如果data.value是数组[time, value],取第二个元素作为数值
- valueStr = data.value[1];
- } else {
- // 如果data.value是单个数值
- valueStr = data.value;
- }
- return `时间: ${timeStr}<br/>数值: ${valueStr}`;
- }
- }
- });
- } catch (error) {
- console.error(`[MultiChartManager] 图表配置设置失败 - 变量 ${varName}:`, error);
- // 销毁失败的实例并移除DOM
- chartInstance.dispose();
- chartDiv.remove();
- return;
- }
-
- // 存储图表实例和数据
- this.charts.set(varName, chartInstance);
- this.chartData.set(varName, []);
-
- // 初始化时间戳数组
- this.varTimestamps.set(varName, []);
-
- // 初始化该变量的更新时间戳
- this.lastUpdateTime.set(varName, 0);
-
- // 为Y轴范围计算提供chartData引用
- chartInstance.chartData = this.chartData.get(varName);
-
- // 强制resize
- setTimeout(() => {
- chartInstance.resize();
- }, 100);
-
- console.log(`[MultiChartManager] 为变量 ${varName} 创建了图表:`, {
- '当前图表数量': this.charts.size,
- '图表实例': !!chartInstance,
- '图表DOM': !!chartDom,
- '数据数组': this.chartData.get(varName),
- '时间戳数组': this.varTimestamps.get(varName),
- '容器子元素数量': this.container.children.length,
- '图表容器可见性': chartDiv.style.display,
- '图表内容高度': chartDom.style.height,
- '图表容器位置': chartDiv.offsetTop + 'px'
- });
- }
-
- /**
- * 检查指定变量是否有图表
- */
- hasChart(varName) {
- const hasChart = this.charts.has(varName);
- const chartInstance = this.charts.get(varName);
- const chartData = this.chartData.get(varName);
-
- console.log(`[MultiChartManager] 检查图表 ${varName}:`, {
- 'hasChart': hasChart,
- 'chartInstance': !!chartInstance,
- 'chartData': !!chartData,
- '数据长度': chartData ? chartData.length : 0
- });
-
- return hasChart;
- }
-
- /**
- * 更新指定变量的图表数据
- */
- updateChartData(varName, newData) {
- // 检查容器是否准备好
- if (!this.container) {
- console.log(`[MultiChartManager] 容器未准备好,延迟更新数据: ${varName}`);
- setTimeout(() => this.updateChartData(varName, newData, timestamp), 100);
- return;
- }
-
- const chartInstance = this.charts.get(varName);
- const chartData = this.chartData.get(varName);
-
- if (!chartInstance || !chartData) {
- // console.log(`[MultiChartManager] 变量 ${varName} 的图表或数据未找到 - chartInstance:`, !!chartInstance, 'chartData:', !!chartData);
- return;
- }
-
- // console.log(`[MultiChartManager] 更新变量 ${varName} 的数据:`, newData, '当前数据长度:', chartData.length);
-
- // 环形缓冲区:固定大小,自动覆盖旧数据
- const bufferSize = 6000;
-
- // 添加新数据到数组
- chartData.push(...newData);
-
- // 如果数据超过缓冲区大小,自动移除最旧的数据
- if (chartData.length > bufferSize) {
- const removeCount = chartData.length - bufferSize;
- chartData.splice(0, removeCount);
- }
-
- // 更新图表实例的chartData引用,确保Y轴范围计算能正常工作
- chartInstance.chartData = chartData;
-
- // 调试:显示数据累积状态
- console.log(`[MultiChartManager] 数据累积 - 变量 ${varName}:`, {
- '新数据': newData,
- '累积后长度': chartData.length,
- '缓冲区大小': bufferSize,
- '数据示例': chartData.slice(-3) // 显示最后3个数据点
- });
-
-
- // 计算时间数据,使用真实的时间戳
- let timeData;
-
- // 为每个数据点生成时间戳(如果没有时间戳数组,则创建)
- if (!this.varTimestamps.has(varName)) {
- this.varTimestamps.set(varName, []);
- }
-
- const timestamps = this.varTimestamps.get(varName);
-
- // 为每个新数据点添加时间戳
- for (let i = 0; i < newData.length; i++) {
- const currentTime = performance.now();
- timestamps.push(currentTime);
- }
-
- // 限制时间戳数量,与数据保持一致
- if (timestamps.length > bufferSize) {
- const removeCount = timestamps.length - bufferSize;
- timestamps.splice(0, removeCount);
- }
-
- // 计算相对时间(只显示可视区间内的数据)
- if (timestamps.length > 0) {
- const currentTime = performance.now();
- const visibleStartTime = currentTime - (this.timeDisplayRange * 1000); // 可视区间的开始时间
-
- // 只保留可视区间内的数据点
- const visibleTimestamps = timestamps.filter(ts => ts >= visibleStartTime);
- const visibleChartData = chartData.slice(-visibleTimestamps.length);
-
- // 计算相对时间(从可视区间开始时间开始)
- timeData = visibleTimestamps.map(ts => (ts - visibleStartTime) / 1000);
-
- // 更新图表数据为可视区间内的数据
- chartData.length = 0;
- chartData.push(...visibleChartData);
-
- // 更新时间戳为可视区间内的时间戳
- timestamps.length = 0;
- timestamps.push(...visibleTimestamps);
- } else {
- timeData = [];
- }
-
- // 调试信息:显示时间数据(限制频率)
- // if (timeData.length > 1 && timeData.length % 50 === 0) {
- // const lastTwoTime = timeData.slice(-2);
- // console.log(`[MultiChartManager] 变量 ${varName} 时间数据:`,
- // `前一个: ${lastTwoTime[0].toFixed(3)}s, 当前: ${lastTwoTime[1].toFixed(3)}s, 间隔: ${(lastTwoTime[1] - lastTwoTime[0]).toFixed(3)}s`);
- // }
-
- // 调试信息:显示滑动窗口信息(限制频率)
- // if (timestamps.length > 1000 && timestamps.length % 100 === 0) {
- // const windowStart = timestamps[0];
- // const windowEnd = timestamps[timestamps.length - 1];
- // const windowDuration = (windowEnd - windowStart) / 1000;
- // console.log(`[MultiChartManager] 变量 ${varName} 滑动窗口:`,
- // `起始: ${(windowStart / 1000).toFixed(3)}s, 结束: ${(windowEnd / 1000).toFixed(3)}s, 窗口长度: ${windowDuration.toFixed(3)}s`);
- // }
-
- // 更新图表 - 使用批量更新和节流绘制
- // 确保数据格式正确,避免白屏
- if (!timeData || timeData.length === 0 || !chartData || chartData.length === 0) {
- console.warn(`[MultiChartManager] 数据不完整,跳过图表更新 - 变量 ${varName}:`, {
- 'timeData长度': timeData ? timeData.length : 0,
- 'chartData长度': chartData ? chartData.length : 0
- });
- return;
- }
-
- const chartDataArray = chartData.map((value, index) => [timeData[index], value]);
-
- // 调试:显示数据格式
- console.log(`[MultiChartManager] 图表数据准备 - 变量 ${varName}:`, {
- '数据点数量': timeData.length,
- '数据数组长度': chartData.length,
- '时间数据长度': timeData.length,
- '图表数据数组': chartDataArray.slice(-3), // 显示最后3个数据点
- '时间范围': timeData.length > 0 ? `${Math.min(...timeData).toFixed(3)}s - ${Math.max(...timeData).toFixed(3)}s` : '无'
- });
-
- // 节流绘制:限制更新频率,避免1000Hz时卡死
- const now = performance.now();
- const lastUpdateTime = this.lastUpdateTime.get(varName) || 0;
- const updateInterval = Math.max(50, 1000 / Math.min(this.samplingRate, 60)); // 最少50ms更新一次,最多60Hz
-
- console.log(`[MultiChartManager] 节流检查 - 变量 ${varName}:`, {
- '当前时间': now.toFixed(1),
- '上次更新时间': lastUpdateTime.toFixed(1),
- '时间差': (now - lastUpdateTime).toFixed(1) + 'ms',
- '更新间隔': updateInterval.toFixed(1) + 'ms',
- '是否允许更新': (now - lastUpdateTime >= updateInterval)
- });
-
- if (now - lastUpdateTime >= updateInterval) {
- // 使用增量更新,只更新数据,不重新配置整个图表
- const option = {
- series: [{
- data: chartDataArray,
- type: 'line',
- smooth: true,
- symbol: 'none',
- lineStyle: { width: 2, color: '#3498db' },
- name: '实时数据'
- }],
- // 确保包含所有必要的配置,避免白屏
- animation: false,
- // 确保网格配置存在
- grid: {
- left: 80,
- right: 40,
- top: 50,
- bottom: 80,
- containLabel: true
- },
- // 保持X轴配置,但更新数据范围
- xAxis: {
- type: 'value',
- name: '时间 (秒)',
- nameLocation: 'middle',
- nameGap: 30,
- // 设置X轴范围,固定显示0到timeDisplayRange
- min: 0,
- max: this.timeDisplayRange,
- // 启用时间轴滚动
- axisLabel: {
- formatter: function(value) {
- return value.toFixed(1) + 's';
- }
- }
- },
- // 保持Y轴配置
- yAxis: {
- type: 'value',
- scale: true,
- name: '数值',
- nameLocation: 'middle',
- nameGap: 40,
- // 动态计算Y轴范围
- min: (() => {
- if (chartData.length > 0) {
- const minVal = Math.min(...chartData);
- const maxVal = Math.max(...chartData);
- const range = maxVal - minVal;
-
- // 如果是1字节数据,设置合适的范围
- if (minVal >= -128 && maxVal <= 127) {
- return Math.max(-130, minVal - 2);
- }
-
- // 对于大数值,使用更合理的余量计算
- if (range > 0) {
- // 使用数据范围的5%作为余量,但最小1,最大1000
- const margin = Math.max(1, Math.min(1000, range * 0.05));
- return minVal - margin;
- } else {
- // 如果数据没有变化,使用固定余量
- return minVal - Math.abs(minVal) * 0.01;
- }
- }
- return 0;
- })(),
- max: (() => {
- if (chartData.length > 0) {
- const minVal = Math.min(...chartData);
- const maxVal = Math.max(...chartData);
- const range = maxVal - minVal;
-
- // 如果是1字节数据,设置合适的范围
- if (minVal >= -128 && maxVal <= 127) {
- return Math.min(130, maxVal + 2);
- }
-
- // 对于大数值,使用更合理的余量计算
- if (range > 0) {
- // 使用数据范围的5%作为余量,但最小1,最大1000
- const margin = Math.max(1, Math.min(1000, range * 0.05));
- return maxVal + margin;
- } else {
- // 如果数据没有变化,使用固定余量
- return maxVal + Math.abs(maxVal) * 0.01;
- }
- }
- return 100;
- })(),
- // 动态设置Y轴标签格式
- axisLabel: {
- formatter: function(value) {
- // 如果是1字节范围,显示整数
- if (value >= -128 && value <= 127) {
- return Math.round(value);
- }
- // 对于大数值,使用更简洁的格式
- if (Math.abs(value) >= 1000) {
- return value.toFixed(0);
- }
- // 对于中等数值,保留1位小数
- if (Math.abs(value) >= 10) {
- return value.toFixed(1);
- }
- // 对于小数值,保留2位小数
- return value.toFixed(2);
- }
- }
- },
- // 保持网格配置
- grid: {
- left: 80,
- right: 40,
- top: 50,
- bottom: 80,
- containLabel: true
- },
- // 保持工具提示配置
- tooltip: {
- trigger: 'axis',
- formatter: function(params) {
- const data = params[0];
- const timeStr = params[0].axisValue || '未知时间';
- // 确保数值正确显示
- let valueStr;
- if (Array.isArray(data.value)) {
- // 如果data.value是数组[time, value],取第二个元素作为数值
- valueStr = data.value[1];
- } else {
- // 如果data.value是单个数值
- valueStr = data.value;
- }
- return `时间: ${timeStr}<br/>数值: ${valueStr}`;
- }
- },
- // 确保图表类型配置存在
- type: 'line'
- };
-
- // 使用增量更新,提高性能
- try {
- chartInstance.setOption(option, true);
- console.log(`[MultiChartManager] 图表更新成功 - 变量 ${varName}:`, {
- '选项配置': option,
- '图表实例状态': !!chartInstance,
- '图表DOM状态': !!chartInstance.getDom()
- });
- } catch (error) {
- console.error(`[MultiChartManager] 图表更新失败 - 变量 ${varName}:`, error);
- // 如果增量更新失败,尝试完全重新设置
- try {
- chartInstance.setOption(option, false);
- console.log(`[MultiChartManager] 图表重新设置成功 - 变量 ${varName}`);
- } catch (retryError) {
- console.error(`[MultiChartManager] 图表重新设置也失败 - 变量 ${varName}:`, retryError);
- }
- }
-
- // 记录更新时间(为每个变量单独记录)
- this.lastUpdateTime.set(varName, now);
-
- // 调试:显示绘制结果
- console.log(`[MultiChartManager] 图表更新完成 - 变量 ${varName}:`, {
- '数据点数量': timeData.length,
- '更新间隔': updateInterval.toFixed(1) + 'ms',
- '采样率': this.samplingRate + 'Hz',
- '绘制频率': (1000 / updateInterval).toFixed(1) + 'Hz',
- '图表实例状态': !!chartInstance,
- '图表数据长度': chartInstance.getOption().series[0].data.length,
- '容器子元素数量': this.container.children.length,
- '图表DOM元素': !!this.container.querySelector(`[data-var="${varName}"]`)
- });
- }
-
- // 调试信息:显示X轴范围(限制频率,避免过多日志)
- // if (timeData.length > 0 && timeData.length % 100 === 0) { // 每100个数据点输出一次
- // const xMin = Math.min(...timeData);
- // const xMax = Math.max(...timeData);
- // console.log(`[MultiChartManager] 变量 ${varName} 图表更新完成,数据点: ${timeData.length}, X轴范围: ${xMin.toFixed(3)}s - ${xMax.toFixed(3)}s`);
-
- // // 显示实际时间范围变化
- // if (timestamps.length > 1) {
- // const firstTime = (timestamps[0] / 1000).toFixed(3);
- // const lastTime = (timestamps[timestamps.length - 1] / 1000).toFixed(3);
- // console.log(`[MultiChartManager] 变量 ${varName} 图表更新完成,数据点: ${timeData.length}, X轴范围: ${xMin.toFixed(3)}s - ${xMax.toFixed(3)}s`);
- // }
- // }
- }
-
- /**
- * 删除指定变量的图表
- */
- removeChart(varName) {
- const chartInstance = this.charts.get(varName);
- if (chartInstance) {
- chartInstance.dispose();
- }
-
- this.charts.delete(varName);
- this.chartData.delete(varName);
- this.chartTimeData.delete(varName);
- this.varTimestamps.delete(varName);
-
- // 移除DOM元素
- const chartElement = this.container.querySelector(`[data-var="${varName}"]`);
- if (chartElement) {
- chartElement.remove();
- }
-
- // 如果没有其他图表,显示默认图表
- if (this.charts.size === 0 && this.defaultChart) {
- this.defaultChart.style.display = 'block';
- }
-
- // console.log(`删除了变量 ${varName} 的图表`);
- }
-
- /**
- * 清空所有图表
- */
- clearAllCharts() {
- // 销毁所有图表实例
- this.charts.forEach((chartInstance, varName) => {
- chartInstance.dispose();
- });
- this.charts.clear();
- this.chartData.clear();
- this.chartTimeData.clear();
- this.varTimestamps.clear();
-
- // 清空容器中的所有图表DOM元素
- const chartElements = this.container.querySelectorAll('.variable-chart');
- chartElements.forEach(element => {
- element.remove();
- });
-
-
- // 确保默认图表可见
- if (this.defaultChart) {
- this.defaultChart.style.display = 'block';
- }
-
- // console.log('已清空所有多变量图表');
- }
-
- /**
- * 设置采样率
- */
- setSamplingRate(rate) {
- this.samplingRate = rate;
- console.log(`[MultiChartManager] 采样率设置为 ${rate}Hz`);
- }
-
- /**
- * 设置时间显示范围
- */
- setTimeDisplayRange(range) {
- this.timeDisplayRange = range;
- console.log(`[MultiChartManager] 时间显示范围设置为 ${range}秒`);
-
- // 立即更新所有图表的X轴范围
- this.charts.forEach((chartInstance, varName) => {
- if (chartInstance && chartInstance.getOption) {
- const option = chartInstance.getOption();
- const chartData = this.chartData.get(varName);
- const timestamps = this.varTimestamps.get(varName);
-
- if (chartData && timestamps && timestamps.length > 0) {
- const startTime = timestamps[0];
- const timeData = timestamps.map(ts => (ts - startTime) / 1000);
- const maxTime = Math.max(...timeData);
-
- // 更新X轴范围
- option.xAxis = {
- ...option.xAxis,
- min: 0,
- max: this.timeDisplayRange
- };
-
- chartInstance.setOption(option);
- }
- }
- });
- }
-
-
- /**
- * 全屏显示指定变量的图表
- */
- fullscreenChart(varName) {
- const chartInstance = this.charts.get(varName);
- if (!chartInstance) {
- console.warn(`[MultiChartManager] 变量 ${varName} 的图表不存在`);
- return;
- }
-
- // 获取图表DOM元素
- const chartElement = this.container.querySelector(`[data-var="${varName}"]`);
- if (!chartElement) {
- console.warn(`[MultiChartManager] 变量 ${varName} 的图表DOM元素不存在`);
- return;
- }
-
- // 创建全屏容器
- const fullscreenContainer = document.createElement('div');
- fullscreenContainer.id = 'fullscreenChartContainer';
- fullscreenContainer.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- background: rgba(0, 0, 0, 0.9);
- z-index: 9999;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- `;
-
- // 创建全屏内容
- fullscreenContainer.innerHTML = `
- <div style="width: 95vw; height: 90vh; background: white; border-radius: 8px; position: relative;">
- <div style="position: absolute; top: 10px; right: 10px; z-index: 10000;">
- <button class="btn btn-danger btn-sm" onclick="document.getElementById('fullscreenChartContainer').remove()" style="font-size: 16px; padding: 8px 12px;">
- ✕ 关闭全屏
- </button>
- </div>
- <div id="fullscreenChartContent" style="width: 100%; height: 100%; padding: 20px;"></div>
- </div>
- `;
-
- // 添加到页面
- document.body.appendChild(fullscreenContainer);
-
- // 创建新的ECharts实例
- const fullscreenChartDom = document.getElementById('fullscreenChartContent');
- const fullscreenChartInstance = echarts.init(fullscreenChartDom);
-
- // 复制原图表的配置和数据
- const originalOption = chartInstance.getOption();
- fullscreenChartInstance.setOption(originalOption);
-
- // 监听窗口大小变化,自动调整图表大小
- const resizeHandler = () => {
- fullscreenChartInstance.resize();
- };
- window.addEventListener('resize', resizeHandler);
-
- // 监听全屏容器移除事件,清理事件监听器
- const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- if (mutation.type === 'childList') {
- mutation.removedNodes.forEach((node) => {
- if (node.id === 'fullscreenChartContainer') {
- window.removeEventListener('resize', resizeHandler);
- observer.disconnect();
- }
- });
- }
- });
- });
- observer.observe(document.body, { childList: true });
-
- console.log(`[MultiChartManager] 变量 ${varName} 图表已全屏显示`);
- }
- }
|