@@ -35,6 +35,32 @@ import { Radio } from "./radio";
35
35
import { RangeSensor , State } from "./state" ;
36
36
import { ModuleWrapper } from "./wasm" ;
37
37
38
+ enum StopKind {
39
+ /**
40
+ * The main Wasm function returned control to us in a normal way.
41
+ */
42
+ Default = "default" ,
43
+ /**
44
+ * The program called panic.
45
+ */
46
+ Panic = "panic" ,
47
+ /**
48
+ * The program requested a reset.
49
+ */
50
+ Reset = "reset" ,
51
+ /**
52
+ * An internal mode where we do not display the stop state UI as we plan to immediately reset.
53
+ * Used for user-requested flash or reset.
54
+ */
55
+ BriefStop = "brief" ,
56
+ /**
57
+ * The user requested the program be interrupted.
58
+ *
59
+ * Note the program could finish for other reasons, but should always count as a user stop.
60
+ */
61
+ UserStop = "user" ,
62
+ }
63
+
38
64
export class PanicError extends Error {
39
65
constructor ( public code : number ) {
40
66
super ( "panic" ) ;
@@ -74,8 +100,6 @@ export class Board {
74
100
radio : Radio ;
75
101
dataLogging : DataLogging ;
76
102
77
- private panicTimeout : any ;
78
-
79
103
public serialInputBuffer : number [ ] = [ ] ;
80
104
81
105
private stoppedOverlay : HTMLDivElement ;
@@ -99,19 +123,32 @@ export class Board {
99
123
return result ?? id ;
100
124
} ;
101
125
126
+ /**
127
+ * Defined for the duration of start().
128
+ */
129
+ private runningPromise : Promise < void > | undefined ;
102
130
/**
103
131
* Defined during start().
104
132
*/
105
133
private modulePromise : Promise < ModuleWrapper > | undefined ;
106
134
/**
107
- * Defined by start but async .
135
+ * Defined during start() .
108
136
*/
109
137
private module : ModuleWrapper | undefined ;
110
138
/**
111
- * If undefined, then when main finishes we stay stopped.
112
- * Otherwise we perform the action then clear this field.
139
+ * Controls the action after the user program completes.
140
+ *
141
+ * Determined by a combination of user actions (stop, reset etc) and program actions.
142
+ */
143
+ private stopKind : StopKind = StopKind . Default ;
144
+ /**
145
+ * Timeout for a pending start call due to StopKind.Reset.
146
+ */
147
+ private pendingRestartTimeout : any ;
148
+ /**
149
+ * Timeout for the next frame of the panic animation.
113
150
*/
114
- private afterStopped : ( ( ) => void ) | undefined ;
151
+ private panicTimeout : any ;
115
152
116
153
constructor (
117
154
private notifications : Notifications ,
@@ -352,10 +389,24 @@ export class Board {
352
389
this . stoppedOverlay . style . display = "flex" ;
353
390
}
354
391
355
- private async start ( ) {
392
+ /**
393
+ * Start the simulator.
394
+ *
395
+ * @returns a promise that resolves when the simulator has stopped.
396
+ */
397
+ private start ( ) : void {
398
+ if ( this . runningPromise ) {
399
+ throw new Error ( "Already running!" ) ;
400
+ }
401
+ this . runningPromise = this . createRunningPromise ( ) ;
402
+ }
403
+
404
+ private async createRunningPromise ( ) {
356
405
if ( this . modulePromise || this . module ) {
357
406
throw new Error ( "Module already exists!" ) ;
358
407
}
408
+ clearTimeout ( this . pendingRestartTimeout ) ;
409
+ this . pendingRestartTimeout = null ;
359
410
360
411
this . modulePromise = this . createModule ( ) ;
361
412
const module = await this . modulePromise ;
@@ -365,11 +416,17 @@ export class Board {
365
416
this . displayRunningState ( ) ;
366
417
await module . start ( ) ;
367
418
} catch ( e : any ) {
419
+ // Take care not to overwrite another kind of stop just because the program
420
+ // called restart or panic.
368
421
if ( e instanceof PanicError ) {
369
- panicCode = e . code ;
422
+ if ( this . stopKind === StopKind . Default ) {
423
+ this . stopKind = StopKind . Panic ;
424
+ panicCode = e . code ;
425
+ }
370
426
} else if ( e instanceof ResetError ) {
371
- const noChangeRestart = ( ) => { } ;
372
- this . afterStopped = noChangeRestart ;
427
+ if ( this . stopKind === StopKind . Default ) {
428
+ this . stopKind = StopKind . Reset ;
429
+ }
373
430
} else {
374
431
this . notifications . onInternalError ( e ) ;
375
432
}
@@ -386,30 +443,61 @@ export class Board {
386
443
this . modulePromise = undefined ;
387
444
this . module = undefined ;
388
445
389
- if ( panicCode !== undefined ) {
390
- this . displayPanic ( panicCode ) ;
391
- } else {
392
- if ( this . afterStopped ) {
393
- this . afterStopped ( ) ;
394
- this . afterStopped = undefined ;
395
- setTimeout ( ( ) => this . start ( ) , 0 ) ;
396
- } else {
446
+ switch ( this . stopKind ) {
447
+ case StopKind . Panic : {
448
+ if ( panicCode === undefined ) {
449
+ throw new Error ( "Must be set" ) ;
450
+ }
451
+ this . displayPanic ( panicCode ) ;
452
+ break ;
453
+ }
454
+ case StopKind . Reset : {
455
+ this . pendingRestartTimeout = setTimeout ( ( ) => this . start ( ) , 0 ) ;
456
+ break ;
457
+ }
458
+ case StopKind . BriefStop : {
459
+ // Skip the stopped state.
460
+ break ;
461
+ }
462
+ case StopKind . UserStop : /* Fall through */
463
+ case StopKind . Default : {
397
464
this . displayStoppedState ( ) ;
465
+ break ;
466
+ }
467
+ default : {
468
+ throw new Error ( "Unknown stop kind: " + this . stopKind ) ;
398
469
}
399
470
}
471
+ this . stopKind = StopKind . Default ;
472
+ this . runningPromise = undefined ;
400
473
}
401
474
402
- async stop (
403
- afterStopped : ( ( ) => void ) | undefined = undefined
404
- ) : Promise < void > {
405
- this . afterStopped = afterStopped ;
475
+ /**
476
+ * Stop the simulator.
477
+ *
478
+ * This cancels any pending restart or panic requested by the program.
479
+ *
480
+ * @param brief If true the stopped UI is not shown.
481
+ * @returns A promise that resolves when the simulator is stopped.
482
+ */
483
+ async stop ( brief : boolean = false ) : Promise < void > {
406
484
if ( this . panicTimeout ) {
407
485
clearTimeout ( this . panicTimeout ) ;
408
486
this . panicTimeout = null ;
409
487
this . display . clear ( ) ;
410
- this . displayStoppedState ( ) ;
488
+ if ( ! brief ) {
489
+ this . displayStoppedState ( ) ;
490
+ }
491
+ }
492
+ if ( this . pendingRestartTimeout ) {
493
+ clearTimeout ( this . pendingRestartTimeout ) ;
494
+ this . pendingRestartTimeout = null ;
495
+ if ( ! brief ) {
496
+ this . displayStoppedState ( ) ;
497
+ }
411
498
}
412
499
if ( this . modulePromise ) {
500
+ this . stopKind = brief ? StopKind . BriefStop : StopKind . UserStop ;
413
501
// Avoid this.module as we might still be creating it (async).
414
502
const module = await this . modulePromise ;
415
503
module . requestStop ( ) ;
@@ -418,15 +506,15 @@ export class Board {
418
506
// Ctrl-C, Ctrl-D to interrupt the main loop.
419
507
this . writeSerialInput ( "\x03\x04" ) ;
420
508
}
509
+ return this . runningPromise ;
421
510
}
422
511
423
512
/**
424
513
* An external reset.
425
- * reset() in MicroPython code throws ResetError.
426
514
*/
427
515
async reset ( ) : Promise < void > {
428
- const noChangeRestart = ( ) => { } ;
429
- this . stop ( noChangeRestart ) ;
516
+ await this . stop ( true ) ;
517
+ this . start ( ) ;
430
518
}
431
519
432
520
async flash ( filesystem : Record < string , Uint8Array > ) : Promise < void > {
@@ -438,10 +526,8 @@ export class Board {
438
526
} ) ;
439
527
this . dataLogging . delete ( ) ;
440
528
} ;
441
- if ( this . modulePromise ) {
442
- // If it's running then we need to stop before flash.
443
- return this . stop ( flashFileSystem ) ;
444
- }
529
+ // Ensure it's stopped before flash.
530
+ await this . stop ( true ) ;
445
531
flashFileSystem ( ) ;
446
532
return this . start ( ) ;
447
533
}
@@ -586,7 +672,7 @@ export class Board {
586
672
587
673
writeRadioRxBuffer ( packet : Uint8Array ) : number {
588
674
if ( ! this . module ) {
589
- throw new Error ( "Must be running" ) ;
675
+ throw new Error ( "Must be running as called via HAL " ) ;
590
676
}
591
677
return this . module . writeRadioRxBuffer ( packet ) ;
592
678
}
0 commit comments