@@ -37,11 +37,35 @@ export const isChromeOS105 = (): boolean => {
3737 return / C r O S / . test ( userAgent ) && / C h r o m e \/ 1 0 5 \b / . test ( userAgent ) ;
3838} ;
3939
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+
4056export interface MicrobitWebUSBConnectionOptions {
4157 // We should copy this type when extracting a library, and make it optional.
4258 // Coupling for now to make it easy to evolve.
4359
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 ;
4569}
4670
4771export interface MicrobitWebUSBConnection
@@ -176,16 +200,17 @@ class MicrobitWebUSBConnectionImpl
176200 } ;
177201
178202 private logging : Logging ;
203+ private deviceSelectionMode : DeviceSelectionMode ;
179204
180205 private addedListeners : Record < string , number > = {
181206 serialdata : 0 ,
182207 } ;
183208
184- constructor (
185- options : MicrobitWebUSBConnectionOptions = { logging : new NullLogging ( ) } ,
186- ) {
209+ constructor ( options : MicrobitWebUSBConnectionOptions = { } ) {
187210 super ( ) ;
188- this . logging = options . logging ;
211+ this . logging = options . logging || new NullLogging ( ) ;
212+ this . deviceSelectionMode =
213+ options . deviceSelectionMode || DeviceSelectionMode . AlwaysAsk ;
189214 }
190215
191216 private log ( v : any ) {
@@ -460,25 +485,86 @@ class MicrobitWebUSBConnectionImpl
460485 }
461486
462487 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 ) ;
466495 }
467- await withTimeout ( this . connection . reconnectAsync ( ) , 10_000 ) ;
468496 if ( this . addedListeners . serialdata && ! this . flashing ) {
469497 this . startSerialInternal ( ) ;
470498 }
471499 this . setStatus ( ConnectionStatus . CONNECTED ) ;
472500 }
473501
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+ }
477523 }
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 > {
478564 this . dispatchTypedEvent ( "beforerequestdevice" , new BeforeRequestDevice ( ) ) ;
479565 this . device = await navigator . usb . requestDevice ( {
480566 exclusionFilters : this . exclusionFilters ,
481- filters : [ { vendorId : 0x0d28 , productId : 0x0204 } ] ,
567+ filters : defaultFilters ,
482568 } ) ;
483569 this . dispatchTypedEvent ( "afterrequestdevice" , new AfterRequestDevice ( ) ) ;
484570 return this . device ;
@@ -509,6 +595,65 @@ class MicrobitWebUSBConnectionImpl
509595 }
510596}
511597
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+
512657const genericErrorSuggestingReconnect = ( e : any ) =>
513658 new DeviceError ( {
514659 code : "reconnect-microbit" ,
0 commit comments