22import { useCallback , useRef , useState , useEffect , useLayoutEffect } from "react" ;
33import {
44 CH55xBootloader ,
5+ FakeBootloader ,
56 parseIntelHexBrowser ,
7+ type BootloaderClient ,
68 type ConnectedInfo ,
79 type Progress ,
810 readFileAsText ,
@@ -338,6 +340,7 @@ export default function CH55xBootloaderMinimal() {
338340 const [ status , setStatus ] = useState < Status > ( { state : "idle" } ) ;
339341 const [ connectedInfo , setConnectedInfo ] = useState < ConnectedInfo | null > ( null ) ;
340342 const [ progress , setProgress ] = useState < Progress > ( { phase : "" , current : 0 , total : 0 } ) ;
343+ const [ demoMode , setDemoMode ] = useState < boolean > ( false ) ;
341344 const [ devMode , setDevMode ] = useState < boolean > ( false ) ;
342345 const [ debugFirmware , setDebugFirmware ] = useState < boolean > ( false ) ;
343346 const [ selectedProfile , setSelectedProfile ] = useState < KnownDeviceProfile | null > ( null ) ;
@@ -361,7 +364,7 @@ export default function CH55xBootloaderMinimal() {
361364
362365 const fileInputRef = useRef < HTMLInputElement | null > ( null ) ;
363366 const hiddenKeyInputRef = useRef < HTMLInputElement | null > ( null ) ;
364- const clientRef = useRef < CH55xBootloader | null > ( null ) ;
367+ const clientRef = useRef < BootloaderClient | null > ( null ) ;
365368 const lastBootloaderIdRef = useRef < number [ ] | null > ( null ) ;
366369 const highlightTimerRef = useRef < number | null > ( null ) ;
367370 const removeTimerRef = useRef < Map < string , number > > ( new Map ( ) ) ;
@@ -433,6 +436,57 @@ export default function CH55xBootloaderMinimal() {
433436 setCurrentBindings ( nextBindings ) ;
434437 } , [ ] ) ;
435438
439+ const applyConnectedDevice = useCallback ( ( info : ConnectedInfo , options : { source : "real" | "demo" ; persistLastId : boolean } ) => {
440+ const previousId = lastBootloaderIdRef . current ;
441+ const sameDevice = sameBootloaderId ( previousId , info . id ) ;
442+
443+ if ( options . persistLastId ) {
444+ lastBootloaderIdRef . current = info . id ;
445+ }
446+
447+ setConnectedInfo ( info ) ;
448+ const profile = findProfileForBootloaderId ( info . id ) ;
449+ setSelectedProfile ( profile ) ;
450+
451+ if ( options . persistLastId ) {
452+ setRememberedBootloaderId ( info . id ) ;
453+ saveLastBootloaderId ( info . id ) ;
454+ }
455+
456+ const stored = loadStoredConfig ( info . id ) ;
457+ if ( ! sameDevice || ! selectedLayout ) {
458+ const nextLayout = stored ?. layout ?? ( profile ?. layout ? cloneLayout ( profile . layout ) : null ) ;
459+ setSelectedLayout ( nextLayout ) ;
460+ }
461+
462+ const nextBindings = stored ?. bindings ?? profile ?. defaultBindings ?? null ;
463+ if ( ! sameDevice || ! currentBindings ) {
464+ setCurrentBindings ( nextBindings ) ;
465+ }
466+
467+ const detail = profile
468+ ? `${ options . source === "demo" ? "Demo: " : "" } ${ profile . name } `
469+ : ( options . source === "demo" ? "Demo device" : undefined ) ;
470+ setStatus ( profile ? { state : "connectedKnown" , detail } : { state : "connectedUnknown" , detail } ) ;
471+ } , [ currentBindings , selectedLayout ] ) ;
472+
473+ const restoreSavedConfig = useCallback ( ( ) => {
474+ const id = rememberedBootloaderId ?? lastBootloaderIdRef . current ;
475+ if ( ! id ) {
476+ setSelectedProfile ( null ) ;
477+ setSelectedLayout ( null ) ;
478+ setCurrentBindings ( null ) ;
479+ return ;
480+ }
481+ const profile = findProfileForBootloaderId ( id ) ;
482+ setSelectedProfile ( profile ) ;
483+ const stored = loadStoredConfig ( id ) ;
484+ const nextLayout = stored ?. layout ?? ( profile ?. layout ? cloneLayout ( profile . layout ) : null ) ;
485+ const nextBindings = stored ?. bindings ?? profile ?. defaultBindings ?? null ;
486+ setSelectedLayout ( nextLayout ) ;
487+ setCurrentBindings ( nextBindings ) ;
488+ } , [ rememberedBootloaderId ] ) ;
489+
436490 const disconnectClient = useCallback ( async ( nextStatus ?: Status , reboot ?: boolean ) => {
437491 const client = clientRef . current ;
438492 if ( client ) {
@@ -442,10 +496,12 @@ export default function CH55xBootloaderMinimal() {
442496 await client . disconnect ( ) . catch ( ( ) => { } ) ;
443497 }
444498 clientRef . current = null ;
499+ setDemoMode ( false ) ;
445500 setConnectedInfo ( null ) ;
446501 setProgress ( { phase : "" , current : 0 , total : 0 } ) ;
502+ restoreSavedConfig ( ) ;
447503 if ( nextStatus ) setStatus ( nextStatus ) ;
448- } , [ ] ) ;
504+ } , [ restoreSavedConfig ] ) ;
449505
450506 const handlePassiveDisconnect = useCallback ( async ( detail ?: string ) => {
451507 await disconnectClient ( { state : "deviceLost" , detail : detail ?? "Device disconnected. Reconnect to continue." } ) ;
@@ -480,43 +536,52 @@ export default function CH55xBootloaderMinimal() {
480536 } ;
481537 } , [ capturingStepIndex ] ) ;
482538
539+ const handleDemoToggle = useCallback ( async ( ) => {
540+ if ( demoMode ) {
541+ await disconnectClient ( { state : "idle" } ) ;
542+ return ;
543+ }
544+
545+ try {
546+ setStatus ( { state : "requesting" , detail : "Starting demo…" } ) ;
547+ if ( clientRef . current ) {
548+ await clientRef . current . disconnect ( ) . catch ( ( ) => { } ) ;
549+ }
550+ const client = new FakeBootloader ( ) ;
551+ clientRef . current = client ;
552+ const info = await client . connect ( ) ;
553+ setDemoMode ( true ) ;
554+ applyConnectedDevice ( info , { source : "demo" , persistLastId : false } ) ;
555+ } catch ( err ) {
556+ clientRef . current = null ;
557+ setDemoMode ( false ) ;
558+ setStatus ( { state : "error" , detail : String ( ( err as Error ) . message ?? "Failed to start demo mode." ) } ) ;
559+ }
560+ } , [ demoMode , disconnectClient , applyConnectedDevice ] ) ;
561+
483562 const handleConnect = useCallback ( async ( ) => {
484563 if ( ! webUsbAvailable ) {
485564 setStatus ( { state : "error" , detail : "WebUSB not available in this browser." } ) ;
486565 return ;
487566 }
488567 try {
489568 setStatus ( { state : "requesting" } ) ;
490- const client = clientRef . current ?? new CH55xBootloader ( ) ;
569+ if ( demoMode && clientRef . current ) {
570+ await clientRef . current . disconnect ( ) . catch ( ( ) => { } ) ;
571+ }
572+
573+ const existing = clientRef . current ;
574+ const client = existing instanceof CH55xBootloader ? existing : new CH55xBootloader ( ) ;
491575 clientRef . current = client ;
576+
577+ setDemoMode ( false ) ;
492578 const info = await client . connect ( ) ;
493- const previousId = lastBootloaderIdRef . current ;
494- const sameDevice = sameBootloaderId ( previousId , info . id ) ;
495- lastBootloaderIdRef . current = info . id ;
496- setConnectedInfo ( info ) ;
497- const profile = findProfileForBootloaderId ( info . id ) ;
498- setRememberedBootloaderId ( info . id ) ;
499- saveLastBootloaderId ( info . id ) ;
500- setSelectedProfile ( profile ) ;
501- const stored = loadStoredConfig ( info . id ) ;
502- if ( ! sameDevice || ! selectedLayout ) {
503- const nextLayout = stored ?. layout ?? ( profile ?. layout ? cloneLayout ( profile . layout ) : null ) ;
504- setSelectedLayout ( nextLayout ) ;
505- }
506- const nextBindings = stored ?. bindings ?? profile ?. defaultBindings ?? null ;
507- if ( ! sameDevice || ! currentBindings ) {
508- setCurrentBindings ( nextBindings ) ;
509- }
510- if ( profile ) {
511- setStatus ( { state : "connectedKnown" , detail : profile . name } ) ;
512- } else {
513- setStatus ( { state : "connectedUnknown" } ) ;
514- }
579+ applyConnectedDevice ( info , { source : "real" , persistLastId : true } ) ;
515580 } catch ( e ) {
516581 const msg = normalizeUsbErrorMessage ( String ( ( e as Error ) . message ?? e ) ) ;
517582 setStatus ( { state : "error" , detail : msg } ) ;
518583 }
519- } , [ webUsbAvailable , selectedLayout , currentBindings ] ) ;
584+ } , [ webUsbAvailable , demoMode , applyConnectedDevice ] ) ;
520585
521586 const handleDisconnect = useCallback ( async ( ) => {
522587 await disconnectClient ( { state : "idle" } , true ) ;
@@ -542,10 +607,12 @@ export default function CH55xBootloaderMinimal() {
542607 } , [ disconnectClient ] ) ;
543608
544609 useEffect ( ( ) => {
545- const targetId = connectedInfo ?. id ?? rememberedBootloaderId ;
610+ if ( demoMode ) return ;
611+ if ( connectedInfo ) return ; // avoid overwriting saved presets while live-connected
612+ const targetId = rememberedBootloaderId ;
546613 if ( ! targetId ) return ;
547614 saveStoredConfig ( targetId , { bindings : currentBindings , layout : selectedLayout } ) ;
548- } , [ connectedInfo , rememberedBootloaderId , currentBindings , selectedLayout ] ) ;
615+ } , [ connectedInfo , rememberedBootloaderId , currentBindings , selectedLayout , demoMode ] ) ;
549616
550617 useEffect ( ( ) => {
551618 if ( ! webUsbAvailable || typeof navigator === "undefined" || ! navigator . usb ) return ;
@@ -563,6 +630,7 @@ export default function CH55xBootloaderMinimal() {
563630 } , [ webUsbAvailable , handlePassiveDisconnect ] ) ;
564631
565632 useEffect ( ( ) => {
633+ if ( demoMode ) return ;
566634 if ( ! connectedInfo || ! clientRef . current ) return ;
567635 if ( status . state === "flashing" || status . state === "compiling" || status . state === "requesting" ) return ;
568636 let cancelled = false ;
@@ -579,7 +647,7 @@ export default function CH55xBootloaderMinimal() {
579647 cancelled = true ;
580648 window . clearInterval ( intervalId ) ;
581649 } ;
582- } , [ connectedInfo , status . state , handlePassiveDisconnect ] ) ;
650+ } , [ connectedInfo , status . state , handlePassiveDisconnect , demoMode ] ) ;
583651
584652 const handlePickFile = useCallback ( ( ) => {
585653 if ( ! clientRef . current ) {
@@ -1307,6 +1375,16 @@ export default function CH55xBootloaderMinimal() {
13071375
13081376 < div className = "actions" >
13091377 < button onClick = { handleConnect } className = "btn" > Connect</ button >
1378+ { ! demoMode && (
1379+ < button
1380+ onClick = { handleDemoToggle }
1381+ className = "btn btn-demo"
1382+ disabled = { Boolean ( connectedInfo ) }
1383+ title = "Start demo mode to explore the UI without connecting a real device."
1384+ >
1385+ Start Demo
1386+ </ button >
1387+ ) }
13101388 { connectedInfo && (
13111389 < button onClick = { handleDisconnect } className = "btn" > Disconnect</ button >
13121390 ) }
@@ -1355,6 +1433,7 @@ export default function CH55xBootloaderMinimal() {
13551433 < div className = "detected-card" >
13561434 < div className = "detected-header" >
13571435 < span className = "pill" > Detected device</ span >
1436+ { demoMode && < span className = "pill pill-demo" > Demo</ span > }
13581437 { ! selectedProfile && < span className = "pill pill-warn" > Unknown</ span > }
13591438 </ div >
13601439 < div className = "detected-name" > { selectedProfile ?. name ?? "Unknown device" } </ div >
0 commit comments