pxt-calliope/editor/flash.ts
Amerlander 3e0c9b43a2
V4 updates for beta testing (#147)
* change simulator svg

* change radio image

* Remove google fonts cdn

* change color of 'advanced' button

* font fix

* font fix 2

* display fix

* change fullsceen simulator bg

* Continuous servo

* handle continuous state

* adding shims

* update rendering for continuous servos

* fixing sim

* fix sig

* typo

* fix sim

* bump pxt

* bump pxt

* rerun travis

* Input blocks revision

- add Button and Pin event types
- merge onPinPressed & onPinReleased in new onPinEvent function
- create new onButtonEvent function

* update input blocks in docs and tests

* remove device_pin_release block

* Hide DAL.x behind Enum

* bring back deprecated blocks, but hide them

* shims and locales files

* fix input.input. typing

* remove buildpr

* bump V3

* update simulator aspect ratio

* add Loudness Block

* revoke loudness block

* Adds soundLevel

To be replaced by pxt-common-packages when DAL is updated.

* Remove P0 & P3 from AnalogPin

* Fix Sound and replace AnalogPin.P0

* remove approved extensions

* V4 Updates from remote Repo

* locales

* add storage functions

* fix storage functions

* fix int/float values

* decrease decimal precision

* reorder blocks

* Update BLE Settings and Storage Blocks

* Fetch MicroBit changes up to v4.0.18

* Update timing for LED Matrix usage

* use 32kb ram (mini v2)

* resize gatt table

* Revert "use 32kb ram (mini v2)"

This reverts commit 4b15592f0f.

* fix missleading indentation

* add support for 32kb and 16kb ram

* only MIT extensions in preferredRepos

* remove extensions without MIT License file

* add updated extensions

* add extensions with MIT license

Co-authored-by: Juri <gitkraken@juriwolf.de>
Co-authored-by: Juri <info@juriwolf.de>
2022-03-22 09:36:19 -07:00

837 lines
30 KiB
TypeScript

const imul = (Math as any).imul;
const timeoutMessage = "timeout";
const membase = 0x20000000;
const loadAddr = membase;
const dataAddr = 0x20002000;
const stackAddr = 0x20001000;
const FULL_FLASH_TIMEOUT = 100000; // 100s
const PARTIAL_FLASH_TIMEOUT = 60000; // 60s
const flashPageBIN = new Uint32Array([
0xbe00be00, // bkpt - LR is set to this
0x2502b5f0, 0x4c204b1f, 0xf3bf511d, 0xf3bf8f6f, 0x25808f4f, 0x002e00ed,
0x2f00595f, 0x25a1d0fc, 0x515800ed, 0x2d00599d, 0x2500d0fc, 0xf3bf511d,
0xf3bf8f6f, 0x25808f4f, 0x002e00ed, 0x2f00595f, 0x2501d0fc, 0xf3bf511d,
0xf3bf8f6f, 0x599d8f4f, 0xd0fc2d00, 0x25002680, 0x00f60092, 0xd1094295,
0x511a2200, 0x8f6ff3bf, 0x8f4ff3bf, 0x2a00599a, 0xbdf0d0fc, 0x5147594f,
0x2f00599f, 0x3504d0fc, 0x46c0e7ec, 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.debug(`dap ${ts}: ${msg}`)
}
const logV = /webusbdbg=1/.test(window.location.href) ? log : (msg: string) => { }
function murmur3_core(data: Uint8Array) {
let h0 = 0x2F9BE6CC;
let h1 = 0x1EC3A6C8;
for (let i = 0; i < data.length; i += 4) {
let k = pxt.HF2.read32(data, i) >>> 0
k = imul(k, 0xcc9e2d51);
k = (k << 15) | (k >>> 17);
k = imul(k, 0x1b873593);
h0 ^= k;
h1 ^= k;
h0 = (h0 << 13) | (h0 >>> 19);
h1 = (h1 << 13) | (h1 >>> 19);
h0 = (imul(h0, 5) + 0xe6546b64) >>> 0;
h1 = (imul(h1, 5) + 0xe6546b64) >>> 0;
}
return [h0, h1]
}
function bufferConcat(a: Uint8Array, b: Uint8Array) {
const r = new Uint8Array(a.length + b.length)
r.set(a, 0)
r.set(b, a.length)
return r
}
class DAPWrapper implements pxt.packetio.PacketIOWrapper {
familyID: number;
private dap: DapJS.DAP;
private cortexM: DapJS.CortexM
private cmsisdap: any;
private flashing = false;
private flashAborted = false;
private readSerialId = 0;
private pbuf = new pxt.U.PromiseBuffer<Uint8Array>();
private pageSize = 1024;
private numPages = 256;
private usesCODAL = false;
private forceFullFlash = /webusbfullflash=1/.test(window.location.href);
private get useJACDAC() {
return this.usesCODAL;
}
onSerial = (buf: Uint8Array, isStderr: boolean) => { };
onCustomEvent = (type: string, payload: Uint8Array) => { };
constructor(public readonly io: pxt.packetio.PacketIO) {
this.familyID = 0x0D28; // this is the microbit vendor id, not quite UF2 family id
this.io.onDeviceConnectionChanged = (connect) => {
log(`device connection changed`);
this.disconnectAsync()
.then(() => connect && this.reconnectAsync());
}
this.io.onData = buf => {
// console.log("RD: " + pxt.Util.toHex(buf))
this.pbuf.push(buf);
}
this.allocDAP();
}
icon = "xicon microbit";
private pendingSerial: Uint8Array
private lastPendingSerial: number
private processSerialLine(line: Uint8Array) {
if (this.onSerial) {
try {
// catch encoding bugs
this.onSerial(line, false)
}
catch (err) {
log(`serial decoding error: ${err.message}`);
pxt.tickEvent("hid.flash.serial.decode.error");
console.error({ err, line })
}
}
}
private async readSerial(): Promise<number> {
let buf = await this.dapCmdNums(0x83)
const len = buf[1]
// concat received data with previous data
if (len) {
buf = buf.slice(2, 2 + len)
if (this.pendingSerial) buf = bufferConcat(this.pendingSerial, buf)
let ptr = 0
let beg = 0
while (ptr < buf.length) {
if (buf[ptr] == 10 || buf[ptr] == 13) {
ptr++;
// eat \r\n
while (ptr < buf.length && (buf[ptr] == 10 || buf[ptr] == 13))
ptr++;
const line = buf.slice(beg, ptr)
if (line.length)
this.processSerialLine(line);
beg = ptr
}
else
ptr++
}
buf = buf.slice(beg)
this.pendingSerial = buf.length ? buf : null
if (this.pendingSerial) {
this.lastPendingSerial = Date.now()
//logV(`pending serial ${this.pendingSerial.length}`)
}
} else if (this.pendingSerial) {
const d = Date.now() - this.lastPendingSerial
if (d > 500) {
this.processSerialLine(this.pendingSerial)
this.pendingSerial = null
}
}
return len
}
private startReadSerial() {
const rid = this.readSerialId;
const startTime = Date.now();
log(`start read serial ${rid}`)
const readSerialLoop = async () => {
try {
while (rid === this.readSerialId) {
const len = await this.readSerial()
const hasData = len > 0
//if (hasData)
// logV(`serial read ${len} bytes`)
await this.jacdacProcess(hasData)
}
log(`stopped serial reader ${rid}`)
} catch (err) {
log(`serial error ${rid}: ${err.message}`);
console.error(err)
if (rid != this.readSerialId) {
log(`stopped serial reader ${rid}`)
} else {
pxt.tickEvent("hid.flash.serial.error");
const timeRunning = Date.now() - startTime
await this.disconnectAsync(); // force disconnect
// if we've been running for a while, try reconnecting
if (timeRunning > 1000) {
log(`auto-reconnect`)
await this.reconnectAsync();
}
}
}
}
readSerialLoop();
}
private stopSerialAsync() {
log(`cancelling serial reader ${this.readSerialId}`)
this.readSerialId++;
return pxt.Util.delay(200);
}
private allocDAP() {
log(`alloc dap`);
const h = this.io;
this.dap = new DapJS.DAP({
write: data => h.sendPacketAsync(new Uint8Array(data)),
close: this.disconnectAsync,
read: () => this.recvPacketAsync(),
//sendMany: sendMany
});
this.cmsisdap = (this.dap as any).dap;
this.cortexM = new DapJS.CortexM(this.dap);
}
get binName() {
return (this.usesCODAL ? "mbcodal-" : "mbdal-") + pxtc.BINARY_HEX;
}
unsupportedParts() {
if (!this.usesCODAL) {
return ["logotouch", "builtinspeaker", "microphone", "flashlog"]
}
return [];
}
async reconnectAsync(): Promise<void> {
log(`reconnect`)
this.flashAborted = false;
function stringResponse(buf: Uint8Array) {
return pxt.U.uint8ArrayToString(buf.slice(2, 2 + buf[1]))
}
await this.stopSerialAsync()
this.allocDAP(); // clean dap apis
await this.io.reconnectAsync()
// before calling into dapjs, we use our dapCmdNums() a few times, which which will make sure the responses
// to commends from previous sessions (if any) are flushed
const info = await this.dapCmdNums(0x00, 0x04) // info
const daplinkVersion = stringResponse(info)
log(`daplink version: ${daplinkVersion}`)
const r = await this.dapCmdNums(0x80)
this.usesCODAL = r[2] == 57 && r[3] == 57 && r[5] >= 51;
const binVersion = stringResponse(r);
log(`bin name: ${this.binName} v:${binVersion}`);
pxt.tickEvent("hid.flash.connect", { codal: this.usesCODAL ? 1 : 0, daplink: daplinkVersion, bin: binVersion });
const baud = new Uint8Array(5)
baud[0] = 0x82 // set baud
pxt.HF2.write32(baud, 1, 115200)
await this.dapCmd(baud)
// setting the baud rate on serial may reset NRF (depending on daplink version), so delay after
await pxt.Util.delay(200);
// only init after setting baud rate, in case we got reset
await this.cortexM.init()
const res = await this.readWords(0x10000010, 2);
this.pageSize = res[0]
this.numPages = res[1]
log(`page size ${this.pageSize}, num pages ${this.numPages}`);
await this.checkStateAsync(true);
await this.jacdacSetup();
this.startReadSerial();
}
private async checkStateAsync(resume?: boolean): Promise<void> {
const states = ["reset", "lockup", "sleeping", "halted", "running"]
try {
const state = await this.cortexM.getState();
log(`cortex state: ${states[state]}`)
if (resume && state == DapJS.CoreState.TARGET_HALTED)
await this.cortexM.resume();
} catch (e) {
log(`cortex state failed`)
pxt.tickEvent("hid.checkstate.error")
console.debug(e)
}
}
private checkAborted() {
if (this.flashAborted)
throw new Error(lf("Download cancelled"));
}
disconnectAsync() {
log(`disconnect`)
this.flashAborted = true;
return this.stopSerialAsync()
.then(() => this.io.disconnectAsync());
}
reflashAsync(resp: pxtc.CompileResult): Promise<void> {
log("reflash")
startTime = 0
pxt.tickEvent("hid.flash.start");
this.flashAborted = false;
this.flashing = true;
return (this.io.isConnected() ? Promise.resolve() : this.io.reconnectAsync())
.then(() => this.stopSerialAsync())
.then(() => this.cortexM.init())
.then(() => this.cortexM.reset(true))
.then(() => this.checkStateAsync())
.then(() => this.readUICR())
.then(uicr => {
pxt.tickEvent("hid.flash.uicr", { uicr });
// shortcut, do a full flash
if (uicr != 0 || this.forceFullFlash) {
pxt.tickEvent("hid.flash.uicrfail");
return this.fullVendorCommandFlashAsync(resp);
}
// check flash checksums
return this.computeFlashChecksum(resp)
.then(chk => {
pxt.tickEvent("hid.flash.checksum", { quick: chk.quick ? 1 : 0, changed: chk.changed ? chk.changed.length : 0 });
// let's do a quick flash!
if (chk.quick)
return this.quickHidFlashAsync(chk.changed);
else
return this.fullVendorCommandFlashAsync(resp);
});
})
.then(() => this.checkStateAsync(true))
.then(() => pxt.tickEvent("hid.flash.success"))
.finally(() => { this.flashing = false })
// don't disconnect here
// the micro:bit will automatically disconnect and reconnect
// via the webusb events
}
private recvPacketAsync() {
if (this.io.recvPacketAsync)
return this.io.recvPacketAsync()
else
return this.pbuf.shiftAsync()
}
private dapCmd(buf: Uint8Array) {
return this.io.sendPacketAsync(buf)
.then(() => this.recvPacketAsync())
.then(resp => {
if (resp[0] != buf[0]) {
pxt.tickEvent('hid.flash.cmderror', { req: buf[0], resp: resp[0] })
const msg = `bad dapCmd response: ${buf[0]} -> ${resp[0]}`
// in case we got an invalid response, try to get another response, in case the current
// response is a left-over from previous communications
log(msg + "; retrying")
return this.recvPacketAsync()
.then(resp => {
if (resp[0] == buf[0])
return resp
throw new Error(msg)
}, err => {
throw new Error(msg)
})
}
return resp
})
}
private dapCmdNums(...nums: number[]) {
return this.dapCmd(new Uint8Array(nums))
}
private fullVendorCommandFlashAsync(resp: pxtc.CompileResult): Promise<void> {
log("full flash")
pxt.tickEvent("hid.flash.full.start");
const chunkSize = 62;
let sentPages = 0;
return pxt.Util.promiseTimeout(
FULL_FLASH_TIMEOUT,
Promise.resolve()
.then(() => this.dapCmdNums(0x8A /* DAPLinkFlash.OPEN */, 1))
.then((res) => {
log(`daplinkflash open: ${pxt.U.toHex(res)}`)
if (res[1] !== 0) {
pxt.tickEvent('hid.flash.full.error.open', { res: res[1] })
throw new Error(lf("Download failed, please try again"));
}
const binFile = resp.outfiles[this.binName];
log(`bin file ${this.binName} in ${Object.keys(resp.outfiles).join(', ')}, ${binFile?.length || -1}b`)
const hexUint8 = pxt.U.stringToUint8Array(binFile);
log(`hex ${hexUint8?.byteLength || -1}b, ~${(hexUint8.byteLength / chunkSize) | 0} chunks of ${chunkSize}b`)
const sendPages = (offset: number = 0): Promise<void> => {
const end = Math.min(hexUint8.length, offset + chunkSize);
const nextPageData = hexUint8.slice(offset, end);
const cmdData = new Uint8Array(2 + nextPageData.length)
cmdData[0] = 0x8C /* DAPLinkFlash.WRITE */
cmdData[1] = nextPageData.length
cmdData.set(nextPageData, 2)
if (sentPages % 128 == 0) // reduce logging
log(`next page ${sentPages}: [${offset.toString(16)}, ${end.toString(16)}] (${Math.ceil((hexUint8.length - end) / 1000)}kb left)`)
return this.dapCmd(cmdData)
.then(() => {
this.checkAborted()
if (end < hexUint8.length) {
sentPages++;
return sendPages(end);
}
return Promise.resolve()
});
}
return sendPages();
})
.then(() => {
log(`close`)
return this.dapCmdNums(0x8B /* DAPLinkFlash.CLOSE */);
})
.then(res => {
log(`daplinkclose: ${pxt.U.toHex(res)}`)
return this.dapCmdNums(0x89 /* DAPLinkFlash.RESET */);
})
.then((res) => {
log(`daplinkreset: ${pxt.U.toHex(res)}`)
log(`full flash done`);
pxt.tickEvent("hid.flash.full.success");
}),
timeoutMessage
).catch((e) => {
log(`error: abort`)
pxt.tickEvent("hid.flash.full.error");
this.flashAborted = true;
return this.resetAndThrowAsync(e);
});
}
private resetAndThrowAsync(e: any) {
log(`reset on error`)
pxt.tickEvent("hid.flash.reset");
console.debug(e)
// reset any pending daplink
return this.dapCmdNums(0x89 /* DAPLinkFlash.RESET */)
.catch((e2: any) => {
// Best effort reset, no-op if there's an error
})
.then(() => this.cortexM.reset(false))
.catch((e2: any) => {
// Best effort reset, no-op if there's an error
})
.then(() => {
throw e;
});
}
private readUICR() {
return this.readWords(0x10001014, 1)
.then(v => {
const uicr = v[0] & 0xff;
log(`uicr: ${uicr.toString(16)} (${v[0].toString(16)})`);
return uicr;
});
}
private computeFlashChecksum(resp: pxtc.CompileResult) {
const binFile = resp.outfiles[this.binName];
if (!binFile)
throw new Error(`unable to find ${this.binName} in outfiles ${Object.keys(resp.outfiles).join(', ')}`);
return this.getFlashChecksumsAsync()
.then(checksums => {
log(`checksums ${pxt.Util.toHex(checksums)}`);
// TODO this is seriously inefficient (130ms on a fast machine)
const uf2 = ts.pxtc.UF2.newBlockFile();
ts.pxtc.UF2.writeHex(uf2, binFile.split(/\r?\n/));
const bytes = pxt.U.stringToUint8Array(ts.pxtc.UF2.serializeFile(uf2));
const parsed = ts.pxtc.UF2.parseFile(bytes);
const aligned = DAPWrapper.pageAlignBlocks(parsed, this.pageSize);
const changed = DAPWrapper.onlyChanged(aligned, checksums, this.pageSize);
const quick = changed.length < aligned.length / 2;
log(`pages: ${aligned.length}, changed ${changed.length}, ${quick ? "quick" : "full"}`);
return {
quick,
changed
}
});
}
private quickHidFlashAsync(changed: ts.pxtc.UF2.Block[]): Promise<void> {
log("quick flash")
pxt.tickEvent("hid.flash.quick.start");
const runFlash = (b: ts.pxtc.UF2.Block, dataAddr: number) => {
const cmd = this.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);
cmd.writeCoreRegister(2, this.pageSize >> 2);
return Promise.resolve()
.then(() => {
logV("setregs")
return cmd.go()
})
.then(() => {
// starts the program
logV(`cortex.debug.enable`)
return this.cortexM.debug.enable()
})
}
return pxt.Util.promiseTimeout(
PARTIAL_FLASH_TIMEOUT,
Promise.resolve()
.then(() => this.cortexM.memory.writeBlock(loadAddr, flashPageBIN))
.then(() => pxt.Util.promiseMapAllSeries(pxt.U.range(changed.length),
i => {
this.checkAborted();
let b = changed[i];
if (b.targetAddr >= 0x10000000) {
log(`target address 0x${b.targetAddr.toString(16)} > 0x10000000`)
return Promise.resolve();
}
log(`about to write at 0x${b.targetAddr.toString(16)}`);
let thisAddr = (i & 1) ? dataAddr : dataAddr + this.pageSize;
let nextAddr = (i & 1) ? dataAddr + this.pageSize : dataAddr;
let writeBl;
if (i == 0) {
let u32data = new Uint32Array(b.data.length / 4);
for (let i = 0; i < b.data.length; i += 4)
u32data[i >> 2] = pxt.HF2.read32(b.data, i);
writeBl = this.cortexM.memory.writeBlock(thisAddr, u32data);
}
if (i == 0) {
let u32data = new Uint32Array(b.data.length / 4);
for (let i = 0; i < b.data.length; i += 4)
u32data[i >> 2] = pxt.HF2.read32(b.data, i);
writeBl = this.cortexM.memory.writeBlock(thisAddr, u32data);
}
return writeBl
.then(() => runFlash(b, thisAddr))
.then(() => {
let next = changed[i + 1];
if (!next)
return Promise.resolve();
logV("write next");
let buf = new Uint32Array(next.data.buffer);
return this.cortexM.memory.writeBlock(nextAddr, buf);
})
.then(() => {
logV("wait");
return this.cortexM.waitForHalt(500);
})
.then(() => {
logV("done block");
});
}))
.then(() => {
log("quick flash done");
return this.cortexM.reset(false);
})
.then(() => {
pxt.tickEvent("hid.flash.quick.success");
return this.checkStateAsync(true)
}),
timeoutMessage
).catch((e) => {
pxt.tickEvent("hid.flash.quick.error");
this.flashAborted = true;
return this.resetAndThrowAsync(e);
});
}
private getFlashChecksumsAsync() {
log("flash checksums")
let pages = this.numPages
return this.cortexM.runCode(computeChecksums2, loadAddr, loadAddr + 1, 0xffffffff, stackAddr, true,
dataAddr, 0, this.pageSize, pages)
.then(() => this.cortexM.memory.readBlock(dataAddr, pages * 2, this.pageSize))
}
private readWords(addr: number, numWords: number) {
return this.cortexM.memory.readBlock(addr, numWords, this.pageSize)
// assume browser is little-endian
.then(u8 => new Uint32Array(u8.buffer))
}
private writeWords(addr: number, buf: Uint32Array) {
return this.cortexM.memory.writeBlock(addr, buf)
}
private readBytes(addr: number, numBytes: number) {
return this.cortexM.memory.readBlock(addr, (numBytes + 3) >> 2, this.pageSize)
.then(u8 => u8.length == numBytes ? u8 : u8.slice(0, numBytes))
}
static onlyChanged(blocks: ts.pxtc.UF2.Block[], checksums: Uint8Array, pageSize: number) {
return blocks.filter(b => {
let idx = b.targetAddr / pageSize
pxt.U.assert((idx | 0) == idx)
pxt.U.assert(b.data.length == pageSize)
if (idx * 8 + 8 > checksums.length)
return true // out of range?
let c0 = pxt.HF2.read32(checksums, idx * 8)
let c1 = pxt.HF2.read32(checksums, idx * 8 + 4)
let ch = murmur3_core(b.data)
if (c0 == ch[0] && c1 == ch[1])
return false
return true
})
}
static pageAlignBlocks(blocks: ts.pxtc.UF2.Block[], pageSize: number) {
pxt.U.assert(pageSize % 256 == 0)
let res: ts.pxtc.UF2.Block[] = []
for (let i = 0; i < blocks.length;) {
let b0 = blocks[i]
let newbuf = new Uint8Array(pageSize)
for (let i = 0; i < newbuf.length; ++i)
newbuf[i] = 0xff
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
pxt.U.memcpy(newbuf, b.targetAddr - newAddr, b.data, 0, b.payloadSize)
}
let bb = pxt.U.flatClone(b0)
bb.data = newbuf
bb.targetAddr = newAddr
bb.payloadSize = pageSize
res.push(bb)
}
return res
}
//
// jacdac stuff starts here
//
private irqn: number
private xchgAddr: number = null
private lastXchg: number
private currSend: SendItem
private sendQ: SendItem[] = []
private lastSend: number
sendCustomEventAsync(type: string, buf: Uint8Array): Promise<void> {
if (type == "jacdac") {
if (this.xchgAddr == null)
return Promise.resolve()
if (buf.length & 3) {
const tmp = new Uint8Array((buf.length + 3) & ~3)
tmp.set(buf)
buf = tmp
}
return new Promise<void>(resolve => {
this.sendQ.push({
buf,
cb: resolve
})
})
}
return Promise.reject(new Error("invalid custom event type"))
}
private writeWord(addr: number, val: number) {
return this.cortexM.memory.write32(addr, val)
}
private async findJacdacXchgAddr() {
const memStart = 0x2000_0000
const memStop = memStart + 128 * 1024
const checkSize = 1024
let p0 = 0x20006000
let p1 = 0x20006000 + checkSize
const check = async (addr: number) => {
if (addr < memStart)
return null
if (addr + checkSize > memStop)
return null
const buf = await this.readWords(addr, checkSize >> 2)
for (let i = 0; i < buf.length; ++i) {
if (buf[i] == 0x786D444A && buf[i + 1] == 0xB0A6C0E9)
return addr + (i << 2)
}
return 0
}
while (true) {
const a0 = await check(p0)
if (a0) return a0
const a1 = await check(p1)
if (a1) return a1
if (a0 === null && a1 === null)
return null
p0 -= checkSize
p1 += checkSize
}
}
private async jacdacSetup() {
this.xchgAddr = null
if (!this.useJACDAC) {
log(`jacdac: disabled`)
return
}
await pxt.Util.delay(700); // wait for the program to start and setup memory correctly
const xchg = await this.findJacdacXchgAddr()
if (xchg == null) {
log("jacdac: xchg address not found")
pxt.tickEvent("hid.flash.jacdac.error.missingxchg");
return
}
const info = await this.readBytes(xchg, 16)
this.irqn = info[8]
if (info[12 + 2] != 0xff) {
log("jacdac: invalid memory; try power-cycling the micro:bit")
pxt.tickEvent("hid.flash.jacdac.error.invalidmemory");
console.debug({ info, xchg })
return
}
this.xchgAddr = xchg
// clear initial lock
await this.writeWord(xchg + 12, 0)
log(`jacdac: exchange address 0x${xchg.toString(16)}; irqn=${this.irqn}`)
pxt.tickEvent("hid.flash.jacdac.connected");
}
private async triggerIRQ(irqn: number) {
const addr = 0xE000E200 + (irqn >> 5) * 4
await this.writeWord(addr, 1 << (irqn & 31))
}
private async jacdacProcess(hadSerial: boolean) {
if (this.xchgAddr == null) {
if (!hadSerial)
await this.dapDelay(5000)
return
}
const now = Date.now()
if (this.lastXchg && now - this.lastXchg > 50) {
logV("slow xchg: " + (now - this.lastXchg) + "ms")
}
this.lastXchg = now
let numev = 0
// TODO only read say 32 bytes first, and more if needed
let inp = await this.readBytes(this.xchgAddr + 12, 256)
if (inp[2]) {
await this.writeWord(this.xchgAddr + 12, 0)
await this.triggerIRQ(this.irqn)
inp = inp.slice(0, inp[2] + 12)
this.onCustomEvent("jacdac", inp)
numev++
}
let sendFree = false
if (this.currSend) {
const send = await this.readBytes(this.xchgAddr + 12 + 256, 4)
if (!send[2]) {
this.currSend.cb()
this.currSend = null
sendFree = true
numev++
}
}
if (!this.currSend && this.sendQ.length) {
if (!sendFree) {
const send = await this.readBytes(this.xchgAddr + 12 + 256, 4)
if (!send[2])
sendFree = true
}
if (sendFree) {
this.currSend = this.sendQ.shift()
const bbody = this.currSend.buf.slice(4)
await this.writeWords(this.xchgAddr + 12 + 256 + 4, new Uint32Array(bbody.buffer))
const bhead = this.currSend.buf.slice(0, 4)
await this.writeWords(this.xchgAddr + 12 + 256, new Uint32Array(bhead.buffer))
await this.triggerIRQ(this.irqn)
this.lastSend = Date.now()
numev++
} else {
if (this.lastSend) {
const d = Date.now() - this.lastSend
if (d > 50) {
this.lastSend = 0
console.error("failed to send packet fast enough")
}
}
}
}
if (numev == 0 && !hadSerial)
await this.dapDelay(5000)
}
private dapDelay(micros: number) {
if (micros > 0xffff)
throw new Error("too large delay")
const cmd = new Uint8Array([0x09, 0, 0])
pxt.HF2.write16(cmd, 1, micros)
return this.dapCmd(cmd)
}
}
interface SendItem {
buf: Uint8Array
cb: () => void
}
export function mkDAPLinkPacketIOWrapper(io: pxt.packetio.PacketIO): pxt.packetio.PacketIOWrapper {
pxt.log(`packetio: mk wrapper dap wrapper`)
return new DAPWrapper(io);
}