2017-04-19 01:26:13 +02:00
/// <reference path="../node_modules/pxt-core/built/pxteditor.d.ts" />
2017-09-18 18:45:27 +02:00
interface Math {
imul ( x : number , y : number ) : number ;
}
2017-04-19 01:26:13 +02:00
namespace pxt . editor {
2017-09-18 18:45:27 +02:00
import UF2 = pxtc . UF2 ;
const pageSize = 1024 ;
const numPages = 256 ;
2018-09-12 16:26:35 +02:00
const timeoutMessage = "timeout" ;
2017-09-18 18:45:27 +02:00
function murmur3_core ( data : Uint8Array ) {
let h0 = 0x2F9BE6CC ;
let h1 = 0x1EC3A6C8 ;
for ( let i = 0 ; i < data . length ; i += 4 ) {
let k = HF2 . read32 ( data , i ) >>> 0
k = Math . imul ( k , 0xcc9e2d51 ) ;
k = ( k << 15 ) | ( k >>> 17 ) ;
k = Math . imul ( k , 0x1b873593 ) ;
h0 ^= k ;
h1 ^= k ;
h0 = ( h0 << 13 ) | ( h0 >>> 19 ) ;
h1 = ( h1 << 13 ) | ( h1 >>> 19 ) ;
h0 = ( Math . imul ( h0 , 5 ) + 0xe6546b64 ) >>> 0 ;
h1 = ( Math . imul ( h1 , 5 ) + 0xe6546b64 ) >>> 0 ;
}
return [ h0 , h1 ]
}
class DAPWrapper {
cortexM : DapJS.CortexM
2017-10-05 07:37:13 +02:00
packetIo : HF2.PacketIO ;
2018-07-27 22:28:58 +02:00
cmsisdap : any ;
flashing = true ;
2018-08-21 20:58:52 +02:00
pbuf = new U . PromiseBuffer < Uint8Array > ( ) ;
2018-07-27 22:28:58 +02:00
private useSerial = true ;
2017-09-18 18:45:27 +02:00
constructor ( h : HF2.PacketIO ) {
2017-10-05 07:37:13 +02:00
this . packetIo = h ;
2017-09-18 18:45:27 +02:00
h . onData = buf = > {
2018-08-21 20:58:52 +02:00
// console.log("RD: " + pxt.Util.toHex(buf))
this . pbuf . push ( buf ) ;
2017-09-18 18:45:27 +02:00
}
2018-08-21 20:58:52 +02:00
this . allocDAP ( )
2018-07-27 22:28:58 +02:00
const readSerial = ( ) = > {
if ( ! this . useSerial ) {
return
}
if ( this . flashing ) {
setTimeout ( readSerial , 300 )
return
}
this . cmsisdap . cmdNums ( 0x83 , [ ] )
. then ( ( r : number [ ] ) = > {
const len = r [ 1 ]
let str = ""
for ( let i = 2 ; i < len + 2 ; ++ i ) {
str += String . fromCharCode ( r [ i ] )
}
if ( str . length > 0 ) {
U . nextTick ( readSerial )
window . postMessage ( {
type : 'serial' ,
id : 'n/a' , // TODO
data : str
} , "*" )
// console.log("SERIAL: " + str)
} else
setTimeout ( readSerial , 50 )
} , ( err : any ) = > {
setTimeout ( readSerial , 1000 )
} )
}
readSerial ( )
2017-09-18 18:45:27 +02:00
}
2018-08-21 20:58:52 +02:00
private allocDAP() {
/ *
let sendMany = ( cmds : Uint8Array [ ] ) = > {
return h . talksAsync ( cmds . map ( c = > ( { cmd : 0 , data : c } ) ) ) ;
}
if ( ! h . talksAsync )
sendMany = null ;
* /
let dev = new DapJS . DAP ( {
write : writeAsync ,
close : this.disconnectAsync ,
read : readAsync ,
//sendMany: sendMany
} ) ;
this . cmsisdap = ( dev as any ) . dap ;
this . cortexM = new DapJS . CortexM ( dev ) ;
let h = this . packetIo
let pbuf = this . pbuf
function writeAsync ( data : ArrayBuffer ) {
// console.log("WR: " + pxt.Util.toHex(new Uint8Array(data)));
return h . sendPacketAsync ( new Uint8Array ( data ) ) ;
}
function readAsync() {
return pbuf . shiftAsync ( ) ;
}
}
2017-09-18 18:45:27 +02:00
reconnectAsync ( first : boolean ) {
2018-08-21 20:58:52 +02:00
// configure serial at 115200
2018-03-21 18:11:56 +01:00
if ( ! first )
return this . packetIo . reconnectAsync ( )
2018-08-21 20:58:52 +02:00
. then ( ( ) = > this . allocDAP ( ) )
2018-03-21 18:11:56 +01:00
. then ( ( ) = > this . cortexM . init ( ) )
2018-08-21 20:58:52 +02:00
. then ( ( ) = > this . cmsisdap . cmdNums ( 0x82 , [ 0x00 , 0xC2 , 0x01 , 0x00 ] ) )
. then ( ( ) = > { } , err = > { this . useSerial = false } )
2018-03-21 18:11:56 +01:00
else
2018-08-21 20:58:52 +02:00
return this . cortexM . init ( )
. then ( ( ) = > this . cmsisdap . cmdNums ( 0x82 , [ 0x00 , 0xC2 , 0x01 , 0x00 ] ) )
. then ( ( ) = > { } , err = > { this . useSerial = false } )
2017-10-05 07:37:13 +02:00
}
disconnectAsync() {
return this . packetIo . disconnectAsync ( ) ;
2017-09-18 18:45:27 +02:00
}
}
2018-02-07 18:42:55 +01:00
let packetIoPromise : Promise < pxt.HF2.PacketIO > ;
function initPacketIOAsync ( ) : Promise < pxt.HF2.PacketIO > {
if ( ! packetIoPromise ) {
packetIoPromise = pxt . HF2 . mkPacketIOAsync ( )
. catch ( err = > {
packetIoPromise = null ;
return Promise . reject ( err ) ;
} ) ;
2018-02-07 21:55:50 +01:00
return packetIoPromise ;
} else {
let packetIo : pxt.HF2.PacketIO ;
return packetIoPromise
. then ( ( io ) = > {
packetIo = io ;
return io . reconnectAsync ( ) ;
} )
. then ( ( ) = > packetIo ) ;
2018-02-07 18:42:55 +01:00
}
}
2017-10-05 07:37:13 +02:00
let previousDapWrapper : DAPWrapper ;
2017-09-18 18:45:27 +02:00
function dapAsync() {
2018-03-21 18:11:56 +01:00
if ( previousDapWrapper )
return Promise . resolve ( previousDapWrapper )
2017-10-05 07:37:13 +02:00
return Promise . resolve ( )
. then ( ( ) = > {
if ( previousDapWrapper ) {
return previousDapWrapper . disconnectAsync ( )
. finally ( ( ) = > {
previousDapWrapper = null ;
} ) ;
}
return Promise . resolve ( ) ;
} )
2018-02-07 18:42:55 +01:00
. then ( ( ) = > initPacketIOAsync ( ) )
2017-09-18 18:45:27 +02:00
. then ( h = > {
let w = new DAPWrapper ( h )
2017-10-05 07:37:13 +02:00
previousDapWrapper = w ;
2017-09-18 18:45:27 +02:00
return w . reconnectAsync ( true )
2018-07-27 22:28:58 +02:00
. then ( ( ) = > {
return w
} )
2017-09-18 18:45:27 +02:00
} )
}
2018-02-26 16:43:02 +01:00
function canHID ( ) : boolean {
let r = false
2018-03-21 18:11:56 +01:00
if ( pxt . usb . isEnabled ) {
r = true
} else if ( U . isNodeJS ) {
2018-02-26 16:43:02 +01:00
r = true
2017-09-18 18:45:27 +02:00
} else {
const forceHexDownload = /forceHexDownload/i . test ( window . location . href ) ;
2017-10-05 07:37:13 +02:00
const isUwp = ! ! ( window as any ) . Windows ;
if ( Cloud . isLocalHost ( ) && Cloud . localToken && ! forceHexDownload || isUwp )
2018-02-26 16:43:02 +01:00
r = true
2017-09-18 18:45:27 +02:00
}
2018-02-26 16:43:02 +01:00
return r ;
}
2017-09-18 18:45:27 +02:00
2018-02-26 16:43:02 +01:00
function initAsync() {
if ( canHID ( ) ) {
2017-10-05 07:37:13 +02:00
return dapAsync ( ) ;
2017-09-18 18:45:27 +02:00
} else {
2017-10-05 07:37:13 +02:00
return Promise . reject ( new Error ( "no HID" ) )
2017-09-18 18:45:27 +02:00
}
}
function pageAlignBlocks ( blocks : UF2.Block [ ] , pageSize : number ) {
U . assert ( pageSize % 256 == 0 )
let res : UF2.Block [ ] = [ ]
for ( let i = 0 ; i < blocks . length ; ) {
let b0 = blocks [ i ]
let newbuf = new Uint8Array ( pageSize )
let startPad = b0 . targetAddr & ( pageSize - 1 )
let newAddr = b0 . targetAddr - startPad
for ( ; i < blocks . length ; ++ i ) {
let b = blocks [ i ]
if ( b . targetAddr + b . payloadSize > newAddr + pageSize )
break
U . memcpy ( newbuf , b . targetAddr - newAddr , b . data , 0 , b . payloadSize )
}
let bb = U . flatClone ( b0 )
bb . data = newbuf
bb . targetAddr = newAddr
bb . payloadSize = pageSize
res . push ( bb )
}
return res
}
const flashPageBINquick = new Uint32Array ( [
0xbe00be00 , // bkpt - LR is set to this
0x2480b5f0 , 0x00e42300 , 0x58cd58c2 , 0xd10342aa , 0x42a33304 , 0xbdf0d1f8 ,
0x4b162502 , 0x509d4a16 , 0x2d00591d , 0x24a1d0fc , 0x511800e4 , 0x3cff3c09 ,
0x591e0025 , 0xd0fc2e00 , 0x509c2400 , 0x2c00595c , 0x2401d0fc , 0x509c2580 ,
0x595c00ed , 0xd0fc2c00 , 0x00ed2580 , 0x002e2400 , 0x5107590f , 0x2f00595f ,
0x3404d0fc , 0xd1f742ac , 0x50992100 , 0x2a00599a , 0xe7d0d0fc , 0x4001e000 ,
0x00000504 ,
] )
// doesn't check if data is already there - for timing
const flashPageBIN = new Uint32Array ( [
0xbe00be00 , // bkpt - LR is set to this
0x2402b5f0 , 0x4a174b16 , 0x2480509c , 0x002500e4 , 0x2e00591e , 0x24a1d0fc ,
0x511800e4 , 0x2c00595c , 0x2400d0fc , 0x2480509c , 0x002500e4 , 0x2e00591e ,
0x2401d0fc , 0x595c509c , 0xd0fc2c00 , 0x00ed2580 , 0x002e2400 , 0x5107590f ,
0x2f00595f , 0x3404d0fc , 0xd1f742ac , 0x50992100 , 0x2a00599a , 0xbdf0d0fc ,
0x4001e000 , 0x00000504 ,
] )
// void computeHashes(uint32_t *dst, uint8_t *ptr, uint32_t pageSize, uint32_t numPages)
const computeChecksums2 = new Uint32Array ( [
0x4c27b5f0 , 0x44a52680 , 0x22009201 , 0x91004f25 , 0x00769303 , 0x24080013 ,
0x25010019 , 0x40eb4029 , 0xd0002900 , 0x3c01407b , 0xd1f52c00 , 0x468c0091 ,
0xa9044665 , 0x506b3201 , 0xd1eb42b2 , 0x089b9b01 , 0x23139302 , 0x9b03469c ,
0xd104429c , 0x2000be2a , 0x449d4b15 , 0x9f00bdf0 , 0x4d149e02 , 0x49154a14 ,
0x3e01cf08 , 0x2111434b , 0x491341cb , 0x405a434b , 0x4663405d , 0x230541da ,
0x4b10435a , 0x466318d2 , 0x230541dd , 0x4b0d435d , 0x2e0018ed , 0x6002d1e7 ,
0x9a009b01 , 0x18d36045 , 0x93003008 , 0xe7d23401 , 0xfffffbec , 0xedb88320 ,
0x00000414 , 0x1ec3a6c8 , 0x2f9be6cc , 0xcc9e2d51 , 0x1b873593 , 0xe6546b64 ,
] )
let startTime = 0
function log ( msg : string ) {
let now = Date . now ( )
if ( ! startTime ) startTime = now
now -= startTime
let ts = ( "00000" + now ) . slice ( - 5 )
pxt . log ( ` HID ${ ts } : ${ msg } ` )
}
const membase = 0x20000000
const loadAddr = membase
const dataAddr = 0x20002000
const stackAddr = 0x20001000
export const bufferConcat = ( bufs : Uint8Array [ ] ) = > {
let len = 0 ;
for ( const b of bufs ) {
len += b . length ;
}
const r = new Uint8Array ( len ) ;
len = 0 ;
for ( const b of bufs ) {
r . set ( b , len ) ;
len += b . length ;
}
return r ;
} ;
2018-09-12 16:26:35 +02:00
function fullVendorCommandFlashAsync ( resp : pxtc.CompileResult , wrap : DAPWrapper ) : Promise < void > {
const chunkSize = 62 ;
let aborted = false ;
2017-09-18 18:45:27 +02:00
2018-09-12 16:26:35 +02:00
return Promise . resolve ( )
. then ( ( ) = > {
return wrap . cmsisdap . cmdNums ( 0x8A /* DAPLinkFlash.OPEN */ , [ 1 ] ) ;
} )
. then ( ( res ) = > {
const hexUint8 = U . stringToUint8Array ( resp . outfiles [ pxtc . BINARY_HEX ] ) ;
const hexArray : number [ ] = Array . prototype . slice . call ( hexUint8 ) ;
const sendPages = ( offset : number = 0 ) : Promise < void > = > {
const end = Math . min ( hexArray . length , offset + chunkSize ) ;
const nextPage = hexArray . slice ( offset , end ) ;
nextPage . unshift ( nextPage . length ) ;
return wrap . cmsisdap . cmdNums ( 0x8C /* DAPLinkFlash.WRITE */ , nextPage )
. then ( ( ) = > {
if ( ! aborted && end < hexArray . length ) {
return sendPages ( end ) ;
}
return Promise . resolve ( ) ;
} ) ;
}
2017-09-18 18:45:27 +02:00
2018-09-12 16:26:35 +02:00
return sendPages ( ) ;
} )
. then ( ( res ) = > {
return wrap . cmsisdap . cmdNums ( 0x8B /* DAPLinkFlash.CLOSE */ , [ ] ) ;
} )
. timeout ( 60000 , timeoutMessage )
. catch ( ( e ) = > {
aborted = true ;
return wrap . cmsisdap . cmdNums ( 0x89 /* DAPLinkFlash.RESET */ , [ ] )
. catch ( ( e2 : any ) = > {
// Best effort reset, no-op if there's an error
} )
. then ( ( ) = > {
return Promise . reject ( e ) ;
} ) ;
} ) ;
2017-09-18 18:45:27 +02:00
}
2018-09-12 16:26:35 +02:00
function quickHidFlashAsync ( resp : pxtc.CompileResult , wrap : DAPWrapper ) : Promise < void > {
2017-09-18 18:45:27 +02:00
let logV = ( msg : string ) = > { }
//let logV = log
2018-09-12 16:26:35 +02:00
let aborted = false ;
2017-09-18 18:45:27 +02:00
const runFlash = ( b : UF2.Block , dataAddr : number ) = > {
const cmd = wrap . cortexM . prepareCommand ( ) ;
cmd . halt ( ) ;
cmd . writeCoreRegister ( DapJS . CortexReg . PC , loadAddr + 4 + 1 ) ;
cmd . writeCoreRegister ( DapJS . CortexReg . LR , loadAddr + 1 ) ;
cmd . writeCoreRegister ( DapJS . CortexReg . SP , stackAddr ) ;
cmd . writeCoreRegister ( 0 , b . targetAddr ) ;
cmd . writeCoreRegister ( 1 , dataAddr ) ;
return Promise . resolve ( )
. then ( ( ) = > {
logV ( "setregs" )
return cmd . go ( )
} )
. then ( ( ) = > {
logV ( "dbg en" )
// starts the program
return wrap . cortexM . debug . enable ( )
} )
}
let checksums : Uint8Array
2018-09-12 16:26:35 +02:00
return getFlashChecksumsAsync ( wrap )
2017-09-18 18:45:27 +02:00
. then ( buf = > {
2018-09-12 16:26:35 +02:00
checksums = buf ;
log ( "write code" ) ;
return wrap . cortexM . memory . writeBlock ( loadAddr , flashPageBIN ) ;
2017-09-18 18:45:27 +02:00
} )
. then ( ( ) = > {
2018-09-12 16:26:35 +02:00
log ( "convert" ) ;
2017-09-18 18:45:27 +02:00
// TODO this is seriously inefficient (130ms on a fast machine)
2018-09-12 16:26:35 +02:00
let uf2 = UF2 . newBlockFile ( ) ;
UF2 . writeHex ( uf2 , resp . outfiles [ pxtc . BINARY_HEX ] . split ( /\r?\n/ ) ) ;
let bytes = U . stringToUint8Array ( UF2 . serializeFile ( uf2 ) ) ;
let parsed = UF2 . parseFile ( bytes ) ;
2017-09-18 18:45:27 +02:00
2018-09-12 16:26:35 +02:00
let aligned = pageAlignBlocks ( parsed , pageSize ) ;
log ( ` initial: ${ aligned . length } pages ` ) ;
aligned = onlyChanged ( aligned , checksums ) ;
log ( ` incremental: ${ aligned . length } pages ` ) ;
2017-09-18 18:45:27 +02:00
return Promise . mapSeries ( U . range ( aligned . length ) ,
i = > {
2018-09-12 16:26:35 +02:00
if ( aborted ) return Promise . resolve ( ) ;
let b = aligned [ i ] ;
2017-09-18 18:45:27 +02:00
if ( b . targetAddr >= 0x10000000 )
2018-09-12 16:26:35 +02:00
return Promise . resolve ( ) ;
2017-09-18 18:45:27 +02:00
2018-09-12 16:26:35 +02:00
logV ( "about to write at 0x" + b . targetAddr . toString ( 16 ) ) ;
2017-09-18 18:45:27 +02:00
2018-09-12 16:26:35 +02:00
let writeBl = Promise . resolve ( ) ;
2017-09-18 18:45:27 +02:00
2018-09-12 16:26:35 +02:00
let thisAddr = ( i & 1 ) ? dataAddr : dataAddr + pageSize ;
let nextAddr = ( i & 1 ) ? dataAddr + pageSize : dataAddr ;
2017-09-18 18:45:27 +02:00
if ( i == 0 ) {
2018-09-12 16:26:35 +02:00
let u32data = new Uint32Array ( b . data . length / 4 ) ;
2017-09-18 18:45:27 +02:00
for ( let i = 0 ; i < b . data . length ; i += 4 )
2018-09-12 16:26:35 +02:00
u32data [ i >> 2 ] = HF2 . read32 ( b . data , i ) ;
writeBl = wrap . cortexM . memory . writeBlock ( thisAddr , u32data ) ;
2017-09-18 18:45:27 +02:00
}
return writeBl
. then ( ( ) = > runFlash ( b , thisAddr ) )
. then ( ( ) = > {
2018-09-12 16:26:35 +02:00
let next = aligned [ i + 1 ] ;
2017-09-18 18:45:27 +02:00
if ( ! next )
2018-09-12 16:26:35 +02:00
return Promise . resolve ( ) ;
logV ( "write next" ) ;
let buf = new Uint32Array ( next . data . buffer ) ;
return wrap . cortexM . memory . writeBlock ( nextAddr , buf ) ;
2017-09-18 18:45:27 +02:00
} )
. then ( ( ) = > {
2018-09-12 16:26:35 +02:00
logV ( "wait" ) ;
return wrap . cortexM . waitForHalt ( 500 ) ;
2017-09-18 18:45:27 +02:00
} )
. then ( ( ) = > {
2018-09-12 16:26:35 +02:00
logV ( "done block" ) ;
} ) ;
2017-09-18 18:45:27 +02:00
} )
. then ( ( ) = > {
2018-09-12 16:26:35 +02:00
log ( "flash done" ) ;
2018-02-20 17:49:28 +01:00
pxt . tickEvent ( "hid.flash.done" ) ;
2018-09-12 16:26:35 +02:00
return wrap . cortexM . reset ( false ) ;
2017-09-18 18:45:27 +02:00
} )
2018-07-27 22:28:58 +02:00
. then ( ( ) = > {
wrap . flashing = false ;
2018-09-12 16:26:35 +02:00
} ) ;
} )
. timeout ( 25000 , timeoutMessage )
. catch ( ( e ) = > {
aborted = true ;
return Promise . reject ( e ) ;
} ) ;
}
2018-09-14 17:25:53 +02:00
function flashAsync ( resp : pxtc.CompileResult , d : pxt.commands.DeployOptions = { } ) : Promise < void > {
2018-09-12 16:26:35 +02:00
startTime = 0
let wrap : DAPWrapper
log ( "init" )
2018-09-14 17:25:53 +02:00
d . showNotification ( U . lf ( "Downloading..." ) ) ;
2018-09-12 16:26:35 +02:00
pxt . tickEvent ( "hid.flash.start" ) ;
return Promise . resolve ( )
. then ( ( ) = > {
if ( previousDapWrapper ) {
previousDapWrapper . flashing = true ;
return Promise . delay ( 100 ) ;
}
return Promise . resolve ( ) ;
} )
. then ( initAsync )
. then ( w = > {
wrap = w
log ( "reset" ) ;
return wrap . cortexM . init ( )
. then ( ( ) = > wrap . cortexM . reset ( true ) )
. catch ( e = > {
log ( "trying re-connect" ) ;
return wrap . reconnectAsync ( false )
. then ( ( ) = > wrap . cortexM . reset ( true ) ) ;
} ) ;
} )
. then ( ( ) = > wrap . cortexM . memory . readBlock ( 0x10001014 , 1 , pageSize ) )
. then ( v = > {
if ( HF2 . read32 ( v , 0 ) != 0x3C000 ) {
pxt . tickEvent ( "hid.flash.uicrfail" ) ;
return fullVendorCommandFlashAsync ( resp , wrap ) ;
}
return quickHidFlashAsync ( resp , wrap ) ;
2017-09-18 18:45:27 +02:00
} )
2018-02-07 18:42:55 +01:00
. catch ( e = > {
2018-09-12 16:26:35 +02:00
if ( e . type === "devicenotfound" && d . reportDeviceNotFoundAsync ) {
2018-02-20 17:49:28 +01:00
pxt . tickEvent ( "hid.flash.devicenotfound" ) ;
2018-09-12 16:26:35 +02:00
return d . reportDeviceNotFoundAsync ( "/device/windows-app/troubleshoot" , resp ) ;
} else if ( e . message === timeoutMessage ) {
pxt . tickEvent ( "hid.flash.timeout" ) ;
return previousDapWrapper . reconnectAsync ( true )
. catch ( ( e ) = > {
// Best effort disconnect; at this point we don't even know the state of the device
pxt . reportException ( e ) ;
} )
. then ( ( ) = > {
return resp . confirmAsync ( {
header : lf ( "Something went wrong..." ) ,
2018-09-14 17:25:53 +02:00
body : lf ( "One-click download took too long. Please disconnect your {0} from your computer and reconnect it, then manually download your program using drag and drop." , pxt . appTarget . appTheme . boardName || lf ( "device" ) ) ,
2018-09-12 16:26:35 +02:00
disagreeLbl : lf ( "Ok" ) ,
hideAgree : true
} ) ;
} )
. then ( ( ) = > {
return pxt . commands . saveOnlyAsync ( resp ) ;
} ) ;
2018-09-14 17:25:53 +02:00
} else if ( e . isUserError ) {
d . reportError ( e . message ) ;
return Promise . resolve ( ) ;
2017-10-17 03:35:45 +02:00
} else {
2018-09-12 16:26:35 +02:00
pxt . tickEvent ( "hid.flash.unknownerror" ) ;
return resp . confirmAsync ( {
2018-09-14 17:25:53 +02:00
header : U.lf ( "Something went wrong..." ) ,
body : U.lf ( "Please manually download your program to your device using drag and drop. One-click download might work afterwards." ) ,
2018-09-12 16:26:35 +02:00
disagreeLbl : lf ( "Ok" ) ,
hideAgree : true
} )
. then ( ( ) = > {
2018-09-14 17:25:53 +02:00
return pxt . commands . saveOnlyAsync ( resp ) ;
2018-09-12 16:26:35 +02:00
} ) ;
2018-02-07 18:42:55 +01:00
}
2018-09-12 16:26:35 +02:00
} ) ;
2017-09-18 18:45:27 +02:00
}
2018-09-14 17:25:53 +02:00
function getFlashChecksumsAsync ( wrap : DAPWrapper ) {
log ( "getting existing flash checksums" )
let pages = numPages
return wrap . cortexM . runCode ( computeChecksums2 , loadAddr , loadAddr + 1 , 0xffffffff , stackAddr , true ,
dataAddr , 0 , pageSize , pages )
. then ( ( ) = > wrap . cortexM . memory . readBlock ( dataAddr , pages * 2 , pageSize ) )
}
function onlyChanged ( blocks : UF2.Block [ ] , checksums : Uint8Array ) {
return blocks . filter ( b = > {
let idx = b . targetAddr / pageSize
U . assert ( ( idx | 0 ) == idx )
U . assert ( b . data . length == pageSize )
if ( idx * 8 + 8 > checksums . length )
return true // out of range?
let c0 = HF2 . read32 ( checksums , idx * 8 )
let c1 = HF2 . read32 ( checksums , idx * 8 + 4 )
let ch = murmur3_core ( b . data )
if ( c0 == ch [ 0 ] && c1 == ch [ 1 ] )
return false
return true
} )
}
export function deployCoreAsync ( resp : pxtc.CompileResult , d : pxt.commands.DeployOptions = { } ) : Promise < void > {
const saveHexAsync = ( ) = > {
return pxt . commands . saveOnlyAsync ( resp ) ;
} ;
return Promise . resolve ( )
. then ( ( ) = > {
const isUwp = ! ! ( window as any ) . Windows ;
if ( isUwp ) {
// Go straight to flashing
return flashAsync ( resp , d ) ;
}
if ( ! pxt . usb . isEnabled ) {
return saveHexAsync ( ) ;
}
return pxt . usb . isPairedAsync ( )
. then ( ( isPaired ) = > {
if ( isPaired ) {
// Already paired from earlier in the session or from previous session
return flashAsync ( resp , d ) ;
}
// No device paired, prompt user
return saveHexAsync ( ) ;
} ) ;
} )
}
2018-05-16 00:10:30 +02:00
/ * *
* < block type = "device_show_leds" >
< field name = "LED00" > FALSE < / field >
< field name = "LED10" > FALSE < / field >
< field name = "LED20" > FALSE < / field >
< field name = "LED30" > FALSE < / field >
< field name = "LED40" > FALSE < / field >
< field name = "LED01" > FALSE < / field >
< field name = "LED11" > FALSE < / field >
< field name = "LED21" > FALSE < / field >
< field name = "LED31" > TRUE < / field >
< field name = "LED41" > FALSE < / field >
< field name = "LED02" > FALSE < / field >
< field name = "LED12" > FALSE < / field >
< field name = "LED22" > FALSE < / field >
< field name = "LED32" > FALSE < / field >
< field name = "LED42" > FALSE < / field >
< field name = "LED03" > FALSE < / field >
< field name = "LED13" > TRUE < / field >
< field name = "LED23" > FALSE < / field >
< field name = "LED33" > FALSE < / field >
< field name = "LED43" > FALSE < / field >
< field name = "LED04" > FALSE < / field >
< field name = "LED14" > FALSE < / field >
< field name = "LED24" > FALSE < / field >
< field name = "LED34" > FALSE < / field >
< field name = "LED44" > FALSE < / field >
< / block >
to
< block type = "device_show_leds" >
< field name = "LEDS" > `
2018-07-27 22:12:36 +02:00
# # # # #
. . . . #
. . . . .
. . . . #
2018-05-16 00:10:30 +02:00
. . . . #
`
< / field >
< / block >
* /
function patchBlocks ( pkgTargetVersion : string , dom : Element ) {
// is this a old script?
if ( pxt . semver . majorCmp ( pkgTargetVersion || "0.0.0" , "1.0.0" ) >= 0 ) return ;
// showleds
const nodes = U . toArray ( dom . querySelectorAll ( "block[type=device_show_leds]" ) )
. concat ( U . toArray ( dom . querySelectorAll ( "block[type=device_build_image]" ) ) )
2018-08-30 23:36:20 +02:00
. concat ( U . toArray ( dom . querySelectorAll ( "shadow[type=device_build_image]" ) ) )
2018-05-16 00:10:30 +02:00
. concat ( U . toArray ( dom . querySelectorAll ( "block[type=device_build_big_image]" ) ) )
2018-08-30 23:36:20 +02:00
. concat ( U . toArray ( dom . querySelectorAll ( "shadow[type=device_build_big_image]" ) ) ) ;
2018-07-27 22:12:36 +02:00
nodes . forEach ( node = > {
2018-06-18 23:28:17 +02:00
// don't rewrite if already upgraded, eg. field LEDS already present
if ( U . toArray ( node . children ) . filter ( child = > child . tagName == "field" && "LEDS" == child . getAttribute ( "name" ) ) [ 0 ] )
return ;
// read LEDxx value and assmebly into a new field
2018-05-16 00:10:30 +02:00
const leds : string [ ] [ ] = [ [ ] , [ ] , [ ] , [ ] , [ ] ] ;
2018-06-12 15:05:18 +02:00
U . toArray ( node . children )
. filter ( child = > child . tagName == "field" && /^LED\d+$/ . test ( child . getAttribute ( "name" ) ) )
2018-05-16 00:10:30 +02:00
. forEach ( lednode = > {
let n = lednode . getAttribute ( "name" ) ;
let col = parseInt ( n [ 3 ] ) ;
let row = parseInt ( n [ 4 ] ) ;
leds [ row ] [ col ] = lednode . innerHTML == "TRUE" ? "#" : "." ;
2018-06-12 15:05:18 +02:00
// remove node
node . removeChild ( lednode ) ;
2018-05-16 00:10:30 +02:00
} ) ;
2018-06-12 15:05:18 +02:00
// add new field
2018-05-16 00:10:30 +02:00
const f = node . ownerDocument . createElement ( "field" ) ;
f . setAttribute ( "name" , "LEDS" ) ;
const s = '`\n' + leds . map ( row = > row . join ( '' ) ) . join ( '\n' ) + '\n`' ;
f . appendChild ( node . ownerDocument . createTextNode ( s ) ) ;
2018-06-12 15:05:18 +02:00
node . insertBefore ( f , null ) ;
2018-05-16 00:10:30 +02:00
} ) ;
// radio
/ *
< block type = "radio_on_packet" x = "174" y = "120" >
< mutation callbackproperties = "receivedNumber" renamemap = "{}" > < / mutation >
< field name = "receivedNumber" > receivedNumber < / field >
< / block >
< block type = "radio_on_packet" disabled = "true" x = "127" y = "263" >
< mutation callbackproperties = "receivedString,receivedNumber" renamemap = "{"receivedString":"name","receivedNumber":"value"}" > < / mutation >
< field name = "receivedString" > name < / field >
< field name = "receivedNumber" > value < / field >
< / block >
< block type = "radio_on_packet" disabled = "true" x = "162" y = "420" >
< mutation callbackproperties = "receivedString" renamemap = "{}" > < / mutation >
< field name = "receivedString" > receivedString < / field >
< / block >
converts to
< block type = "radio_on_number" x = "196" y = "208" >
< field name = "HANDLER_receivedNumber" id = "DCy(W;1)*jLWQUpoy4Mm" variabletype = "" > receivedNumber < / field >
< / block >
< block type = "radio_on_value" x = "134" y = "408" >
< field name = "HANDLER_name" id = "*d-Jm^MJXO]Djs(dTR*?" variabletype = "" > name < / field >
< field name = "HANDLER_value" id = "A6HQjH[k^X43o3h775+G" variabletype = "" > value < / field >
< / block >
< block type = "radio_on_string" x = "165" y = "583" >
< field name = "HANDLER_receivedString" id = "V9KsE!h$(iO?%W:[32CV" variabletype = "" > receivedString < / field >
< / block >
* /
const varids : pxt.Map < string > = { } ;
2018-05-16 00:20:01 +02:00
function addField ( node : Element , renameMap : pxt.Map < string > , name : string ) {
2018-05-16 00:10:30 +02:00
const f = node . ownerDocument . createElement ( "field" ) ;
2018-05-16 00:20:01 +02:00
f . setAttribute ( "name" , "HANDLER_" + name )
f . setAttribute ( "id" , varids [ renameMap [ name ] || name ] ) ;
2018-05-16 00:10:30 +02:00
f . appendChild ( node . ownerDocument . createTextNode ( name ) ) ;
node . appendChild ( f ) ;
}
U . toArray ( dom . querySelectorAll ( "variable" ) ) . forEach ( node = > varids [ node . innerHTML ] = node . getAttribute ( "id" ) ) ;
U . toArray ( dom . querySelectorAll ( "block[type=radio_on_packet]" ) )
. forEach ( node = > {
const mutation = node . querySelector ( "mutation" ) ;
if ( ! mutation ) return ;
const renameMap = JSON . parse ( node . getAttribute ( "renamemap" ) || "{}" ) ;
2018-07-27 22:12:36 +02:00
const props = mutation . getAttribute ( "callbackproperties" ) ;
if ( props ) {
const parts = props . split ( "," ) ;
// It's tempting to generate radio_on_number if parts.length === 0 but
// that would create a variable named "receivedNumber" and possibly shadow
// an existing variable in the user's program. It's safer to stick to the
// old block.
if ( parts . length === 1 ) {
if ( parts [ 0 ] === "receivedNumber" ) {
node . setAttribute ( "type" , "radio_on_number" ) ;
node . removeChild ( node . querySelector ( "field[name=receivedNumber]" ) ) ;
addField ( node , renameMap , "receivedNumber" ) ;
}
else if ( parts [ 0 ] === "receivedString" ) {
node . setAttribute ( "type" , "radio_on_string" ) ;
node . removeChild ( node . querySelector ( "field[name=receivedString]" ) ) ;
addField ( node , renameMap , "receivedString" ) ;
}
else {
return ;
}
node . removeChild ( mutation ) ;
}
else if ( parts . length === 2 && parts . indexOf ( "receivedNumber" ) !== - 1 && parts . indexOf ( "receivedString" ) !== - 1 ) {
2018-05-16 00:10:30 +02:00
node . setAttribute ( "type" , "radio_on_value" ) ;
node . removeChild ( node . querySelector ( "field[name=receivedNumber]" ) ) ;
node . removeChild ( node . querySelector ( "field[name=receivedString]" ) ) ;
2018-05-16 00:20:01 +02:00
addField ( node , renameMap , "name" ) ;
addField ( node , renameMap , "value" ) ;
2018-07-27 22:12:36 +02:00
node . removeChild ( mutation ) ;
}
2018-05-16 00:10:30 +02:00
}
} )
2018-07-30 23:13:28 +02:00
// device_random now refers to randomRange() so we need to add the missing lower bound argument
U . toArray ( dom . querySelectorAll ( "block[type=device_random]" ) )
2018-08-30 23:36:20 +02:00
. concat ( U . toArray ( dom . querySelectorAll ( "shadow[type=device_random]" ) ) )
2018-07-30 23:13:28 +02:00
. forEach ( node = > {
2018-08-13 21:51:26 +02:00
if ( getValue ( node , "min" ) ) return ;
2018-07-30 23:13:28 +02:00
const v = node . ownerDocument . createElement ( "value" ) ;
v . setAttribute ( "name" , "min" ) ;
2018-08-13 21:51:26 +02:00
addNumberShadow ( v ) ;
2018-07-30 23:13:28 +02:00
node . appendChild ( v ) ;
} ) ;
2018-08-13 21:51:26 +02:00
/ *
< block type = "math_arithmetic" >
< field name = "OP" > DIVIDE < / field >
< value name = "A" >
< shadow type = "math_number" > < field name = "NUM" > 0 < / field > < / shadow >
< block type = "math_number" > < field name = "NUM" > 2 < / field > < / block >
< / value >
< value name = "B" >
< shadow type = "math_number" > < field name = "NUM" > 1 < / field > < / shadow >
< block type = "math_number" > < field name = "NUM" > 3 < / field > < / block >
< / value >
< / block >
* /
2018-08-21 20:58:52 +02:00
U . toArray ( dom . querySelectorAll ( "block[type=math_arithmetic]" ) )
2018-08-30 23:36:20 +02:00
. concat ( U . toArray ( dom . querySelectorAll ( "shadow[type=math_arithmetic]" ) ) )
2018-08-13 21:51:26 +02:00
. forEach ( node = > {
const op = getField ( node , "OP" ) ;
if ( ! op || op . textContent . trim ( ) !== "DIVIDE" ) return ;
// Convert to integer division
/ *
< block type = "math_js_op" >
< mutation op - type = "infix" > < / mutation >
< field name = "OP" > idiv < / field >
< value name = "ARG0" >
< shadow type = "math_number" > < field name = "NUM" > 0 < / field > < / shadow >
< / value >
< value name = "ARG1" >
< shadow type = "math_number" > < field name = "NUM" > 0 < / field > < / shadow >
< / value >
< / block >
* /
node . setAttribute ( "type" , "math_js_op" ) ;
op . textContent = "idiv" ;
const mutation = node . ownerDocument . createElement ( "mutation" ) ;
mutation . setAttribute ( "op-type" , "infix" ) ;
// mutation has to be first or Blockly will drop the second argument
node . insertBefore ( mutation , node . firstChild ) ;
const a = getValue ( node , "A" ) ;
if ( a ) a . setAttribute ( "name" , "ARG0" ) ;
const b = getValue ( node , "B" ) ;
if ( b ) b . setAttribute ( "name" , "ARG1" ) ;
2018-08-30 23:36:20 +02:00
} ) ;
2018-08-13 21:51:26 +02:00
2018-08-30 23:36:20 +02:00
// math_number_minmax
U . toArray ( dom . querySelectorAll ( "block[type=math_number_minmax]" ) )
. concat ( U . toArray ( dom . querySelectorAll ( "shadow[type=math_number_minmax]" ) ) )
. forEach ( node = > {
// Change the name of the NUM field to SLIDER
const numField = getField ( node , "NUM" ) ;
if ( numField ) {
numField . setAttribute ( "name" , "SLIDER" ) ;
}
} ) ;
2018-05-16 00:10:30 +02:00
}
2017-09-18 18:45:27 +02:00
initExtensionsAsync = function ( opts : pxt.editor.ExtensionOptions ) : Promise < pxt.editor.ExtensionResult > {
2017-04-19 01:26:13 +02:00
pxt . debug ( 'loading microbit target extensions...' )
2017-09-18 18:45:27 +02:00
if ( ! Math . imul )
2018-07-27 22:28:58 +02:00
Math . imul = function ( a , b ) {
2018-06-12 15:05:18 +02:00
const ah = ( a >>> 16 ) & 0xffff ;
const al = a & 0xffff ;
const bh = ( b >>> 16 ) & 0xffff ;
const bl = b & 0xffff ;
2017-09-18 18:45:27 +02:00
// the shift by 0 fixes the sign on the high part
// the final |0 converts the unsigned value into a signed value
return ( ( al * bl ) + ( ( ( ah * bl + al * bh ) << 16 ) >>> 0 ) | 0 ) ;
} ;
2017-04-19 01:26:13 +02:00
const res : pxt.editor.ExtensionResult = {
hexFileImporters : [ {
id : "blockly" ,
canImport : data = > data . meta . cloudId == "microbit.co.uk" && data . meta . editor == "blockly" ,
importAsync : ( project , data ) = > project . createProjectAsync ( {
filesOverride : {
"main.blocks" : data . source
} , name : data.meta.name
} )
2017-10-05 07:37:13 +02:00
} ]
2017-04-19 01:26:13 +02:00
} ;
2018-02-26 16:43:02 +01:00
2018-03-21 18:11:56 +01:00
pxt . usb . setFilters ( [ {
vendorId : 0x0D28 ,
productId : 0x0204 ,
classCode : 0xff ,
subclassCode : 0x03
} ] )
2018-02-26 16:43:02 +01:00
if ( canHID ( ) )
2018-09-12 16:26:35 +02:00
pxt . commands . deployCoreAsync = deployCoreAsync ;
2018-05-16 00:10:30 +02:00
res . blocklyPatch = patchBlocks ;
2018-09-12 05:50:52 +02:00
res . showUploadInstructionsAsync = showUploadInstructionsAsync ;
2018-09-14 17:25:53 +02:00
res . webUsbPairDialogAsync = webUsbPairDialogAsync ;
2017-04-19 01:26:13 +02:00
return Promise . resolve < pxt.editor.ExtensionResult > ( res ) ;
}
2017-09-18 18:45:27 +02:00
2018-08-13 21:51:26 +02:00
function getField ( parent : Element , name : string ) {
return getFieldOrValue ( parent , name , true ) ;
}
function getValue ( parent : Element , name : string ) {
return getFieldOrValue ( parent , name , false ) ;
}
function getFieldOrValue ( parent : Element , name : string , isField : boolean ) {
const nodeType = isField ? "field" : "value" ;
for ( let i = 0 ; i < parent . children . length ; i ++ ) {
const child = parent . children . item ( i ) ;
if ( child . tagName === nodeType && child . getAttribute ( "name" ) === name ) {
return child ;
}
}
return undefined ;
}
function addNumberShadow ( valueNode : Element ) {
const s = valueNode . ownerDocument . createElement ( "shadow" ) ;
s . setAttribute ( "type" , "math_number" ) ;
const f = valueNode . ownerDocument . createElement ( "field" ) ;
f . setAttribute ( "name" , "NUM" ) ;
f . textContent = "0" ;
s . appendChild ( f ) ;
valueNode . appendChild ( s ) ;
}
2018-09-12 05:50:52 +02:00
2018-09-14 17:25:53 +02:00
function webUsbPairDialogAsync ( confirmAsync : ( options : any ) = > Promise < number > ) : Promise < number > {
2018-09-12 05:50:52 +02:00
const boardName = pxt . appTarget . appTheme . boardName || "???" ;
2018-09-25 20:34:10 +02:00
const docUrl = pxt . appTarget . appTheme . usbDocs ;
2018-09-12 05:50:52 +02:00
const htmlBody = `
< div class = "ui grid stackable" >
2018-09-14 17:25:53 +02:00
< div class = "column five wide" style = "background-color: #FFFFCE;" >
< div class = "ui header" > $ { lf ( "First time here?" ) } < / div >
2018-09-26 16:49:38 +02:00
< strong style = "font-size:small" > $ { lf ( "You must have version 0250 or above of the firmware" ) } < / strong >
2018-09-12 05:50:52 +02:00
< div style = "justify-content: center;display: flex;padding: 1rem;" >
< img class = "ui image" src = "./static/download/firmware.png" style = "height:100px;" / >
< / div >
2018-09-26 16:59:26 +02:00
< a href = "${docUrl}/webusb/troubleshoot" target = "_blank" > $ { lf ( "Check your firmware version here and update if needed" ) } < / a >
2018-09-14 17:25:53 +02:00
< / div >
< div class = "column eleven wide" >
2018-09-12 05:50:52 +02:00
< div class = "ui grid" >
< div class = "row" >
< div class = "column" >
< div class = "ui two column grid padded" >
< div class = "column" >
< div class = "ui" >
< div class = "image" >
< img class = "ui medium rounded image" src = "./static/download/connect.png" style = "margin-bottom:1rem;" / >
< / div >
< div class = "content" >
< div class = "description" >
< span class = "ui purple circular label" > 1 < / span >
< strong > $ { lf ( "Connect the {0} to your computer with a USB cable" , boardName ) } < / strong >
< br / >
< span style = "font-size:small" > $ { lf ( "Use the microUSB port on the top of the {0}" , boardName ) } < / span >
< / div >
< / div >
< / div >
< / div >
< div class = "column" >
< div class = "ui" >
< div class = "image" >
2018-09-14 17:25:53 +02:00
< img class = "ui medium rounded image" src = "./static/download/pair.png" style = "margin-bottom:1rem;" / >
2018-09-12 05:50:52 +02:00
< / div >
< div class = "content" >
< div class = "description" >
< span class = "ui purple circular label" > 2 < / span >
2018-09-14 17:25:53 +02:00
< strong > $ { lf ( "Pair your {0}" , boardName ) } < / strong >
2018-09-12 05:50:52 +02:00
< br / >
2018-09-14 17:25:53 +02:00
< span style = "font-size:small" > $ { lf ( "Click 'Pair device' below and select <strong>BBC micro:bit CMSIS-DAP</strong> or <strong>DAPLink CMSIS-DAP</strong> from the list" ) } < / span >
2018-09-12 05:50:52 +02:00
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div > ` ;
2018-09-14 17:25:53 +02:00
const buttons : any [ ] = [ ] ;
if ( docUrl ) {
buttons . push ( {
label : lf ( "Help" ) ,
icon : "help" ,
className : "lightgrey" ,
url : ` ${ docUrl } /webusb `
} ) ;
}
2018-09-12 05:50:52 +02:00
return confirmAsync ( {
2018-09-14 17:25:53 +02:00
header : lf ( "Pair device for one-click downloads" ) ,
2018-09-12 05:50:52 +02:00
htmlBody ,
hasCloseIcon : true ,
2018-09-14 17:25:53 +02:00
agreeLbl : lf ( "Pair device" ) ,
agreeIcon : "usb" ,
2018-09-12 05:50:52 +02:00
hideCancel : true ,
className : 'downloaddialog' ,
2018-09-14 17:25:53 +02:00
buttons
} ) ;
}
function showUploadInstructionsAsync ( fn : string , url : string , confirmAsync : ( options : any ) = > Promise < number > ) {
2018-09-26 17:43:14 +02:00
const boardName = Util . htmlEscape ( pxt . appTarget . appTheme . boardName || "???" ) ;
const boardDriveName = Util . htmlEscape ( pxt . appTarget . appTheme . driveDisplayName || pxt . appTarget . compile . driveName || "???" ) ;
2018-09-14 17:25:53 +02:00
// https://msdn.microsoft.com/en-us/library/cc848897.aspx
// "For security reasons, data URIs are restricted to downloaded resources.
// Data URIs cannot be used for navigation, for scripting, or to populate frame or iframe elements"
2018-09-26 17:43:14 +02:00
const userDownload = pxt . BrowserUtils . isBrowserDownloadWithinUserContext ( ) ;
2018-09-14 17:25:53 +02:00
const downloadAgain = ! pxt . BrowserUtils . isIE ( ) && ! pxt . BrowserUtils . isEdge ( ) ;
const docUrl = pxt . appTarget . appTheme . usbDocs ;
2018-09-26 17:43:14 +02:00
const body =
userDownload
? lf ( "Click 'Download' to open the {0} app." , pxt . appTarget . appTheme . boardName || "" )
: undefined ;
const htmlBody = ! userDownload ?
` <div class="ui grid stackable">
2018-09-14 17:25:53 +02:00
< div class = "column sixteen wide" >
< div class = "ui grid" >
< div class = "row" >
< div class = "column" >
< div class = "ui two column grid padded" >
< div class = "column" >
< div class = "ui" >
< div class = "image" >
< img class = "ui medium rounded image" src = "./static/download/connect.png" style = "margin-bottom:1rem;" / >
< / div >
< div class = "content" >
< div class = "description" >
< span class = "ui purple circular label" > 1 < / span >
< strong > $ { lf ( "Connect the {0} to your computer with a USB cable" , boardName ) } < / strong >
< br / >
< span style = "font-size:small" > $ { lf ( "Use the microUSB port on the top of the {0}" , boardName ) } < / span >
< / div >
< / div >
< / div >
< / div >
< div class = "column" >
< div class = "ui" >
< div class = "image" >
< img class = "ui medium rounded image" src = "./static/download/transfer.png" style = "margin-bottom:1rem;" / >
< / div >
< div class = "content" >
< div class = "description" >
< span class = "ui purple circular label" > 2 < / span >
< strong > $ { lf ( "Move the .hex file to the {0}" , boardName ) } < / strong >
< br / >
< span style = "font-size:small" > $ { lf ( "Locate the downloaded .hex file and drag it to the <strong>{0}</strong> drive" , boardDriveName ) } < / span >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
2018-09-26 17:43:14 +02:00
< / div > ` : undefined;
2018-09-14 17:25:53 +02:00
const buttons : any [ ] = [ ] ;
if ( downloadAgain ) {
buttons . push ( {
2018-09-26 17:43:14 +02:00
label : userDownload ? lf ( "Download" ) : fn ,
2018-09-12 05:50:52 +02:00
icon : "download" ,
2018-09-26 17:43:14 +02:00
class : ` ${ userDownload ? "primary" : "lightgrey" } ` ,
2018-09-12 05:50:52 +02:00
url ,
fileName : fn
2018-09-14 17:25:53 +02:00
} ) ;
}
if ( docUrl ) {
buttons . push ( {
2018-09-12 05:50:52 +02:00
label : lf ( "Help" ) ,
icon : "help" ,
className : "lightgrey" ,
url : docUrl
2018-09-14 17:25:53 +02:00
} ) ;
}
return confirmAsync ( {
header : lf ( "Download to your {0}" , pxt . appTarget . appTheme . boardName ) ,
2018-09-26 17:43:14 +02:00
body ,
2018-09-14 17:25:53 +02:00
htmlBody ,
hasCloseIcon : true ,
hideCancel : true ,
hideAgree : true ,
className : 'downloaddialog' ,
buttons
2018-09-12 05:50:52 +02:00
//timeout: 20000
} ) . then ( ( ) = > { } ) ;
}
2017-09-18 18:45:27 +02:00
}