namespace pxt . usb { export class USBError extends Error { constructor ( msg ) { super ( msg ) ; this . message = msg ; } } export const enum VID { ATMEL = 0x03EB , ARDUINO = 0x2341 , ADAFRUIT = 0x239A , NXP = 0x0d28 } const controlTransferGetReport = 0x01 ; const controlTransferSetReport = 0x09 ; const controlTransferOutReport = 0x200 ; const controlTransferInReport = 0x100 ; export interface USBDeviceFilter { vendorId ? : number ; productId ? : number ; classCode ? : number ; subclassCode ? : number ; protocolCode ? : number ; serialNumber ? : string ; } export let filters = [ { classCode : 255 , subclassCode : 42 , } ] ; let isHF2 = true ; export function setFilters ( f ) { isHF2 = false ; filters = f ; } export type USBEndpointType = "bulk" | "interrupt" | "isochronous" ; export type USBRequestType = "standard" | "class" | "vendor" ; export type USBRecipient = "device" | "interface" | "endpoint" | "other" ; export type USBTransferStatus = "ok" | "stall" | "babble" ; export type USBDirection = "in" | "out" ; export type BufferSource = Uint8Array ; export interface USBConfiguration { configurationValue : number ; configurationName : string ; interfaces : USBInterface [ ] ; } ; export interface USBInterface { interfaceNumber : number ; alternate : USBAlternateInterface ; alternates : USBAlternateInterface [ ] ; claimed : boolean ; } ; export interface USBAlternateInterface { alternateSetting : number ; interfaceClass : number ; interfaceSubclass : number ; interfaceProtocol : number ; interfaceName : string ; endpoints : USBEndpoint [ ] ; } ; export interface USBEndpoint { endpointNumber : number ; direction : USBDirection ; type : USBEndpointType ; packetSize : number ; } export interface USBControlTransferParameters { requestType : USBRequestType ; recipient : USBRecipient ; request : number ; value : number ; index : number ; } export interface USBInTransferResult { data : { buffer : ArrayBuffer ; } ; status : USBTransferStatus ; } export interface USBOutTransferResult { bytesWritten : number ; status : USBTransferStatus ; } export interface USBIsochronousInTransferPacket { data : DataView ; status : USBTransferStatus ; } export interface USBIsochronousInTransferResult { data : DataView ; packets : USBIsochronousInTransferPacket [ ] ; } export interface USBIsochronousOutTransferPacket { bytesWritten : number ; status : USBTransferStatus ; } export interface USBIsochronousOutTransferResult { packets : USBIsochronousOutTransferPacket [ ] ; } export interface USBDevice { vendorId : number ; productId : number ; manufacturerName : string ; productName : string ; serialNumber : string ; deviceClass : number ; deviceSubclass : number ; deviceProtocol : number ; deviceVersionMajor : number ; deviceVersionMinor : number ; deviceVersionSubminor : number ; usbVersionMajor : number ; usbVersionMinor : number ; usbVersionSubminor : number ; configurations : USBConfiguration [ ] ; opened : boolean ; open ( ) : Promise < void > ; close ( ) : Promise < void > ; selectConfiguration ( configurationValue ) : Promise < void > ; claimInterface ( interfaceNumber ) : Promise < void > ; releaseInterface ( interfaceNumber ) : Promise < void > ; selectAlternateInterface ( interfaceNumber , alternateSetting ) : Promise < void > ; controlTransferIn ( setup , length ) : Promise < USBInTransferResult > ; controlTransferOut ( setup , data ? ) : Promise < USBOutTransferResult > ; clearHalt ( direction , endpointNumber ) : Promise < void > ; transferIn ( endpointNumber , length ) : Promise < USBInTransferResult > ; transferOut ( endpointNumber , data ) : Promise < USBOutTransferResult > ; isochronousTransferIn ( endpointNumber , packetLengths ) : Promise < USBIsochronousInTransferResult > ; isochronousTransferOut ( endpointNumber , data , packetLengths ) : Promise < USBIsochronousOutTransferResult > ; reset ( ) : Promise < void > ; } class WebUSBHID implements pxt . packetio . PacketIO { lastKnownDeviceSerialNumber : string ; dev : USBDevice ; ready = false ; connecting = false ; iface : USBInterface ; altIface : USBAlternateInterface ; epIn : USBEndpoint ; epOut : USBEndpoint ; readLoopStarted = false ; onDeviceConnectionChanged = ( connect ) => { } ; onConnectionChanged = ( ) => { } ; onData = ( v ) => { } ; onError = ( e ) => { } ; onEvent = ( v ) => { } ; enabled = false ; constructor ( ) { this . handleUSBConnected = this . handleUSBConnected . bind ( this ) ; this . handleUSBDisconnected = this . handleUSBDisconnected . bind ( this ) ; } enable ( ) { if ( this . enabled ) return ; this . enabled = true ; this . log ( "registering webusb events" ) ; ( navigator as any ) . usb . addEventListener ( 'disconnect' , this . handleUSBDisconnected , false ) ; ( navigator as any ) . usb . addEventListener ( 'connect' , this . handleUSBConnected , false ) ; } disable ( ) { if ( ! this . enabled ) return ; this . enabled = false ; this . log ( `unregistering webusb events` ) ; ( navigator as any ) . usb . removeEventListener ( 'disconnect' , this . handleUSBDisconnected ) ; ( navigator as any ) . usb . removeEventListener ( 'connect' , this . handleUSBConnected ) ; } disposeAsync ( ) { this . disable ( ) ; return Promise . resolve ( ) ; } private handleUSBDisconnected ( event ) { this . log ( "device disconnected" ) ; if ( event . device == this . dev ) { this . log ( "clear device" ) ; this . clearDev ( ) ; if ( this . onDeviceConnectionChanged ) this . onDeviceConnectionChanged ( false ) ; } } private handleUSBConnected ( event ) { const newdev = event . device as USBDevice ; this . log ( `device connected ${ newdev . serialNumber }` ) ; if ( ! this . dev && ! this . connecting ) { this . log ( "attach device" ) ; if ( this . onDeviceConnectionChanged ) this . onDeviceConnectionChanged ( true ) ; } } private clearDev ( ) { if ( this . dev ) { this . dev = null ; this . epIn = null ; this . epOut = null ; if ( this . onConnectionChanged ) this . onConnectionChanged ( ) ; } } error ( msg ) { throw new USBError ( U . lf ( "USB error on device {0} ({1})" , this . dev . productName , msg ) ) ; } log ( msg ) { pxt . debug ( "webusb: " + msg ) ; } disconnectAsync ( ) { this . ready = false ; if ( ! this . dev ) return Promise . resolve ( ) ; this . log ( "close device" ) ; return this . dev . close ( ) . catch ( e => { } ) . then ( ( ) => { this . clearDev ( ) ; return U . delay ( 500 ) ; } ) ; } reconnectAsync ( ) { this . log ( "reconnect" ) ; this . setConnecting ( true ) ; return this . disconnectAsync ( ) . then ( tryGetDevicesAsync ) . then ( devs => this . connectAsync ( devs ) ) . finally ( ( ) => this . setConnecting ( false ) ) ; } private setConnecting ( v ) { if ( v != this . connecting ) { this . connecting = v ; if ( this . onConnectionChanged ) this . onConnectionChanged ( ) ; } } isConnecting ( ) { return this . connecting ; } isConnected ( ) { return ! ! this . dev && this . ready ; } private async connectAsync ( devs ) { this . log ( `trying to connect (${ devs . length } devices)` ) ; if ( devs . length == 0 ) { const e = new Error ( "Device not found." ) ; ( e as any ) . type = "devicenotfound" ; throw e ; } this . setConnecting ( true ) ; try { if ( this . lastKnownDeviceSerialNumber ) { const lastDev = devs . find ( d => d . serialNumber === this . lastKnownDeviceSerialNumber ) ; if ( lastDev ) { this . log ( `last known device spotted` ) ; devs . splice ( devs . indexOf ( lastDev ) , 1 ) ; devs . unshift ( lastDev ) ; } else { this . log ( `delay for last known device` ) ; await U . delay ( 2000 ) ; } } for ( let i = 0 ; i < devs . length ; ++ i ) { const dev = devs [ i ] ; this . dev = dev ; this . log ( `connect device: ${ dev . manufacturerName } ${ dev . productName }` ) ; this . log ( `serial number: ${ dev . serialNumber } ${ this . lastKnownDeviceSerialNumber === dev . serialNumber ? "(last known device)" : "" } ` ) ; try { await this . initAsync ( ) ; return ; } catch ( e ) { this . dev = undefined ; this . log ( `connection failed, ${ e . message }` ) ; } } const e = new Error ( U . lf ( "Device in use or not found." ) ) ; ( e as any ) . type = "devicelocked" ; throw e ; } finally { this . setConnecting ( false ) ; } } sendPacketAsync ( pkt ) { if ( ! this . dev ) return Promise . reject ( new Error ( "Disconnected" ) ) ; Util . assert ( pkt . length <= 64 ) ; if ( ! this . epOut ) { return this . dev . controlTransferOut ( { requestType : "class" , recipient : "interface" , request : controlTransferSetReport , value : controlTransferOutReport , index : this . iface . interfaceNumber } , pkt ) . then ( res => { if ( res . status != "ok" ) this . error ( "USB CTRL OUT transfer failed" ) ; } ) ; } return this . dev . transferOut ( this . epOut . endpointNumber , pkt ) . then ( res => { if ( res . status != "ok" ) this . error ( "USB OUT transfer failed" ) ; } ) ; } private readLoop ( ) { if ( this . readLoopStarted ) return ; this . readLoopStarted = true ; this . log ( "start read loop" ) ; let loop = ( ) : void => { if ( ! this . ready ) U . delay ( 300 ) . then ( loop ) ; else this . recvPacketAsync ( ) . then ( buf => { if ( buf [ 0 ] ) { this . onData ( buf ) ; loop ( ) ; } else { U . delay ( 500 ) . then ( loop ) ; } } , err => { if ( this . dev ) this . onError ( err ) ; U . delay ( 300 ) . then ( loop ) ; } ) ; } ; loop ( ) ; } recvPacketAsync ( ) { let final = ( res ) => { if ( res . status != "ok" ) this . error ( "USB IN transfer failed" ) ; let arr = new Uint8Array ( res . data . buffer ) ; if ( arr . length == 0 ) return this . recvPacketAsync ( ) ; return arr ; } ; if ( ! this . dev ) return Promise . reject ( new Error ( "Disconnected" ) ) ; if ( ! this . epIn ) { return this . dev . controlTransferIn ( { requestType : "class" , recipient : "interface" , request : controlTransferGetReport , value : controlTransferInReport , index : this . iface . interfaceNumber } , 64 ) . then ( final ) ; } return this . dev . transferIn ( this . epIn . endpointNumber , 64 ) . then ( final ) ; } initAsync ( ) { if ( ! this . dev ) return Promise . reject ( new Error ( "Disconnected" ) ) ; let dev = this . dev ; this . log ( "open device" ) ; return dev . open ( ) . then ( ( ) => { this . log ( "select configuration" ) ; return dev . selectConfiguration ( 1 ) ; } ) . then ( ( ) => { let matchesFilters = ( iface ) => { let a0 = iface . alternates [ 0 ] ; for ( let f of filters ) { if ( f . classCode == null || a0 . interfaceClass === f . classCode ) { if ( f . subclassCode == null || a0 . interfaceSubclass === f . subclassCode ) { if ( f . protocolCode == null || a0 . interfaceProtocol === f . protocolCode ) { if ( a0 . endpoints . length == 0 ) return true ; if ( a0 . endpoints . length == 2 && a0 . endpoints . every ( e => e . packetSize == 64 ) ) return true ; } } } } return false ; } ; this . log ( "got " + dev . configurations [ 0 ] . interfaces . length + " interfaces" ) ; const matching = dev . configurations [ 0 ] . interfaces . filter ( matchesFilters ) ; let iface = matching [ matching . length - 1 ] ; this . log ( `${ matching . length } matching interfaces; picking ${ iface ? "#" + iface . interfaceNumber : "n/a" }` ) ; if ( ! iface ) this . error ( "cannot find supported USB interface" ) ; this . altIface = iface . alternates [ 0 ] ; this . iface = iface ; if ( this . altIface . endpoints . length ) { this . log ( "using dedicated endpoints" ) ; this . epIn = this . altIface . endpoints . filter ( e => e . direction == "in" ) [ 0 ] ; this . epOut = this . altIface . endpoints . filter ( e => e . direction == "out" ) [ 0 ] ; Util . assert ( this . epIn . packetSize == 64 ) ; Util . assert ( this . epOut . packetSize == 64 ) ; } else { this . log ( "using ctrl pipe" ) ; } this . log ( "claim interface" ) ; return dev . claimInterface ( iface . interfaceNumber ) ; } ) . then ( ( ) => { this . log ( "device ready" ) ; this . lastKnownDeviceSerialNumber = this . dev . serialNumber ; this . ready = true ; if ( isHF2 ) this . readLoop ( ) ; if ( this . onConnectionChanged ) this . onConnectionChanged ( ) ; } ) ; } } export function pairAsync ( ) { return ( ( navigator as any ) . usb . requestDevice ( { filters : filters } ) as Promise < USBDevice > ) . then ( dev => ! ! dev ) . catch ( e => { if ( e . name == "NotFoundError" ) return undefined ; throw e ; } ) ; } async function tryGetDevicesAsync ( ) { log ( `webusb: get devices` ) ; try { const devs = await ( ( navigator as any ) . usb . getDevices ( ) as Promise < USBDevice [ ] > ) ; return devs || [ ] ; } catch ( e ) { reportException ( e ) ; return [ ] ; } } let _hid ; export function mkWebUSBHIDPacketIOAsync ( ) { pxt . debug ( `packetio: mk webusb io` ) ; if ( ! _hid ) _hid = new WebUSBHID ( ) ; _hid . enable ( ) ; return Promise . resolve ( _hid ) ; } export let isEnabled = false ; export function setEnabled ( v ) { if ( ! isAvailable ( ) ) v = false ; isEnabled = v ; } let _available = undefined ; export async function checkAvailableAsync ( ) { if ( _available !== undefined ) return ; pxt . debug ( `webusb: checking availability` ) ; if ( ! pxt . appTarget ?. compile ?. webUSB ) { _available = false ; return ; } if ( pxt . BrowserUtils . isElectron ( ) || pxt . BrowserUtils . isWinRT ( ) ) { pxt . debug ( `webusb: off, electron or winrt` ) ; pxt . tickEvent ( 'webusb.off' , { 'reason' : 'electronwinrt' } ) ; _available = false ; return ; } const _usb = ( navigator as any ) . usb ; if ( ! _usb ) { pxt . debug ( `webusb: off, not impl` ) ; pxt . tickEvent ( 'webusb.off' , { 'reason' : 'notimpl' } ) ; _available = false ; return ; } let m = /Windows NT (\d+\.\d+)/ . exec ( navigator . userAgent ) ; if ( m && parseFloat ( m [ 1 ] ) < 6.3 ) { pxt . debug ( `webusb: off, older windows version` ) ; pxt . tickEvent ( 'webusb.off' , { 'reason' : 'oldwindows' } ) ; _available = false ; return ; } try { await _usb . getDevices ( ) ; } catch ( e ) { pxt . debug ( `webusb: off, security exception` ) ; pxt . tickEvent ( 'webusb.off' , { 'reason' : 'security' } ) ; _available = false ; return ; } _available = true ; return ; } export function isAvailable ( ) { if ( _available === undefined ) { console . error ( `checkAvailableAsync not called` ) ; checkAvailableAsync ( ) ; } return ! ! _available ; } }