@@ -37,11 +37,35 @@ export const isChromeOS105 = (): boolean => {
37
37
return / C r O S / . test ( userAgent ) && / C h r o m e \/ 1 0 5 \b / . test ( userAgent ) ;
38
38
} ;
39
39
40
+ const defaultFilters = [ { vendorId : 0x0d28 , productId : 0x0204 } ] ;
41
+
42
+ export enum DeviceSelectionMode {
43
+ /**
44
+ * Attempts to connect to known device, otherwise asks which device to
45
+ * connect to.
46
+ */
47
+ AlwaysAsk = "AlwaysAsk" ,
48
+
49
+ /**
50
+ * Attempts to connect to known device, otherwise attempts to connect to any
51
+ * allowed devices. If that fails, asks which device to connect to.
52
+ */
53
+ UseAnyAllowed = "UseAnyAllowed" ,
54
+ }
55
+
40
56
export interface MicrobitWebUSBConnectionOptions {
41
57
// We should copy this type when extracting a library, and make it optional.
42
58
// Coupling for now to make it easy to evolve.
43
59
44
- logging : Logging ;
60
+ /**
61
+ * Determines logging behaviour for events, errors, and logs.
62
+ */
63
+ logging ?: Logging ;
64
+
65
+ /**
66
+ * Determines how a device should be selected.
67
+ */
68
+ deviceSelectionMode ?: DeviceSelectionMode ;
45
69
}
46
70
47
71
export interface MicrobitWebUSBConnection
@@ -176,16 +200,17 @@ class MicrobitWebUSBConnectionImpl
176
200
} ;
177
201
178
202
private logging : Logging ;
203
+ private deviceSelectionMode : DeviceSelectionMode ;
179
204
180
205
private addedListeners : Record < string , number > = {
181
206
serialdata : 0 ,
182
207
} ;
183
208
184
- constructor (
185
- options : MicrobitWebUSBConnectionOptions = { logging : new NullLogging ( ) } ,
186
- ) {
209
+ constructor ( options : MicrobitWebUSBConnectionOptions = { } ) {
187
210
super ( ) ;
188
- this . logging = options . logging ;
211
+ this . logging = options . logging || new NullLogging ( ) ;
212
+ this . deviceSelectionMode =
213
+ options . deviceSelectionMode || DeviceSelectionMode . AlwaysAsk ;
189
214
}
190
215
191
216
private log ( v : any ) {
@@ -460,25 +485,86 @@ class MicrobitWebUSBConnectionImpl
460
485
}
461
486
462
487
private async connectInternal ( ) : Promise < void > {
463
- if ( ! this . connection ) {
464
- const device = await this . chooseDevice ( ) ;
465
- this . connection = new DAPWrapper ( device , this . logging ) ;
488
+ if ( ! this . connection && this . device ) {
489
+ this . connection = new DAPWrapper ( this . device , this . logging ) ;
490
+ await withTimeout ( this . connection . reconnectAsync ( ) , 10_000 ) ;
491
+ } else if ( ! this . connection ) {
492
+ await this . connectWithOtherDevice ( ) ;
493
+ } else {
494
+ await withTimeout ( this . connection . reconnectAsync ( ) , 10_000 ) ;
466
495
}
467
- await withTimeout ( this . connection . reconnectAsync ( ) , 10_000 ) ;
468
496
if ( this . addedListeners . serialdata && ! this . flashing ) {
469
497
this . startSerialInternal ( ) ;
470
498
}
471
499
this . setStatus ( ConnectionStatus . CONNECTED ) ;
472
500
}
473
501
474
- private async chooseDevice ( ) : Promise < USBDevice > {
475
- if ( this . device ) {
476
- return this . device ;
502
+ private async connectWithOtherDevice ( ) : Promise < void > {
503
+ if ( this . deviceSelectionMode === DeviceSelectionMode . UseAnyAllowed ) {
504
+ await this . attemptConnectAllowedDevices ( ) ;
505
+ }
506
+ if ( ! this . connection ) {
507
+ this . device = await this . chooseDevice ( ) ;
508
+ this . connection = new DAPWrapper ( this . device , this . logging ) ;
509
+ await withTimeout ( this . connection . reconnectAsync ( ) , 10_000 ) ;
510
+ }
511
+ }
512
+
513
+ // Based on: https://github.com/microsoft/pxt/blob/ab97a2422879824c730f009b15d4bf446b0e8547/pxtlib/webusb.ts#L361
514
+ private async attemptConnectAllowedDevices ( ) : Promise < void > {
515
+ const pairedDevices = await this . getFilteredAllowedDevices ( ) ;
516
+ for ( const device of pairedDevices ) {
517
+ const connection = await this . attemptDeviceConnection ( device ) ;
518
+ if ( connection ) {
519
+ this . device = device ;
520
+ this . connection = connection ;
521
+ return ;
522
+ }
477
523
}
524
+ }
525
+
526
+ // Based on: https://github.com/microsoft/pxt/blob/ab97a2422879824c730f009b15d4bf446b0e8547/pxtlib/webusb.ts#L530
527
+ private async getFilteredAllowedDevices ( ) : Promise < USBDevice [ ] > {
528
+ this . log ( "Retrieving previously paired USB devices" ) ;
529
+ try {
530
+ const devices = await this . withEnrichedErrors ( ( ) =>
531
+ navigator . usb ?. getDevices ( ) ,
532
+ ) ;
533
+ if ( devices === undefined ) {
534
+ return [ ] ;
535
+ }
536
+ const filteredDevices = devices . filter ( ( device ) =>
537
+ applyDeviceFilters ( device , defaultFilters , this . exclusionFilters ?? [ ] ) ,
538
+ ) ;
539
+ return filteredDevices ;
540
+ } catch ( error : any ) {
541
+ this . log ( `Failed to retrieve paired devices: ${ error . message } ` ) ;
542
+ return [ ] ;
543
+ }
544
+ }
545
+
546
+ private async attemptDeviceConnection (
547
+ device : USBDevice ,
548
+ ) : Promise < DAPWrapper | undefined > {
549
+ this . log (
550
+ `Attempting connection to: ${ device . manufacturerName } ${ device . productName } ` ,
551
+ ) ;
552
+ this . log ( `Serial number: ${ device . serialNumber } ` ) ;
553
+ try {
554
+ const connection = new DAPWrapper ( device , this . logging ) ;
555
+ await withTimeout ( connection . reconnectAsync ( ) , 10_000 ) ;
556
+ return connection ;
557
+ } catch ( error : any ) {
558
+ this . log ( `Connection attempt failed: ${ error . message } ` ) ;
559
+ return ;
560
+ }
561
+ }
562
+
563
+ private async chooseDevice ( ) : Promise < USBDevice > {
478
564
this . dispatchTypedEvent ( "beforerequestdevice" , new BeforeRequestDevice ( ) ) ;
479
565
this . device = await navigator . usb . requestDevice ( {
480
566
exclusionFilters : this . exclusionFilters ,
481
- filters : [ { vendorId : 0x0d28 , productId : 0x0204 } ] ,
567
+ filters : defaultFilters ,
482
568
} ) ;
483
569
this . dispatchTypedEvent ( "afterrequestdevice" , new AfterRequestDevice ( ) ) ;
484
570
return this . device ;
@@ -509,6 +595,65 @@ class MicrobitWebUSBConnectionImpl
509
595
}
510
596
}
511
597
598
+ /**
599
+ * Applying WebUSB device filter. Exported for testing.
600
+ * Based on: https://wicg.github.io/webusb/#enumeration
601
+ */
602
+ export const applyDeviceFilters = (
603
+ device : USBDevice ,
604
+ filters : USBDeviceFilter [ ] ,
605
+ exclusionFilters : USBDeviceFilter [ ] ,
606
+ ) => {
607
+ return (
608
+ ( filters . length === 0 ||
609
+ filters . some ( ( filter ) => matchFilter ( device , filter ) ) ) &&
610
+ ( exclusionFilters . length === 0 ||
611
+ exclusionFilters . every ( ( filter ) => ! matchFilter ( device , filter ) ) )
612
+ ) ;
613
+ } ;
614
+
615
+ const matchFilter = ( device : USBDevice , filter : USBDeviceFilter ) => {
616
+ if ( filter . vendorId && device . vendorId !== filter . vendorId ) {
617
+ return false ;
618
+ }
619
+ if ( filter . productId && device . productId !== filter . productId ) {
620
+ return false ;
621
+ }
622
+ if ( filter . serialNumber && device . serialNumber !== filter . serialNumber ) {
623
+ return false ;
624
+ }
625
+ return hasMatchingInterface ( device , filter ) ;
626
+ } ;
627
+
628
+ const hasMatchingInterface = ( device : USBDevice , filter : USBDeviceFilter ) => {
629
+ if (
630
+ filter . classCode === undefined &&
631
+ filter . subclassCode === undefined &&
632
+ filter . protocolCode === undefined
633
+ ) {
634
+ return true ;
635
+ }
636
+ if ( ! device . configuration ?. interfaces ) {
637
+ return false ;
638
+ }
639
+ return device . configuration . interfaces . some ( ( configInterface ) => {
640
+ return configInterface . alternates ?. some ( ( alternate ) => {
641
+ const classCodeNotMatch =
642
+ filter . classCode !== undefined &&
643
+ alternate . interfaceClass !== filter . classCode ;
644
+ const subClassCodeNotMatch =
645
+ filter . subclassCode !== undefined &&
646
+ alternate . interfaceSubclass !== filter . subclassCode ;
647
+ const protocolCodeNotMatch =
648
+ filter . protocolCode !== undefined &&
649
+ alternate . interfaceProtocol !== filter . protocolCode ;
650
+ return (
651
+ ! classCodeNotMatch || ! subClassCodeNotMatch || ! protocolCodeNotMatch
652
+ ) ;
653
+ } ) ;
654
+ } ) ;
655
+ } ;
656
+
512
657
const genericErrorSuggestingReconnect = ( e : any ) =>
513
658
new DeviceError ( {
514
659
code : "reconnect-microbit" ,
0 commit comments