419 строки
12 KiB
TypeScript
419 строки
12 KiB
TypeScript
import * as webusb from "webusb"
|
|
import * as U from "./pxtutils"
|
|
|
|
const controlTransferGetReport = 0x01;
|
|
const controlTransferSetReport = 0x09;
|
|
const controlTransferOutReport = 0x200;
|
|
const controlTransferInReport = 0x100;
|
|
|
|
// see https://github.com/microsoft/uf2/blob/master/hf2.md for full spec
|
|
export const HF2_CMD_BININFO = 0x0001 // no arguments
|
|
export const HF2_MODE_BOOTLOADER = 0x01
|
|
export const HF2_MODE_USERSPACE = 0x02
|
|
/*
|
|
struct HF2_BININFO_Result {
|
|
uint32_t mode;
|
|
uint32_t flash_page_size;
|
|
uint32_t flash_num_pages;
|
|
uint32_t max_message_size;
|
|
};
|
|
*/
|
|
|
|
export const HF2_CMD_INFO = 0x0002
|
|
// no arguments
|
|
// results is utf8 character array
|
|
|
|
export const HF2_CMD_RESET_INTO_APP = 0x0003// no arguments, no result
|
|
|
|
export const HF2_CMD_RESET_INTO_BOOTLOADER = 0x0004 // no arguments, no result
|
|
|
|
export const HF2_CMD_START_FLASH = 0x0005 // no arguments, no result
|
|
|
|
export const HF2_CMD_WRITE_FLASH_PAGE = 0x0006
|
|
/*
|
|
struct HF2_WRITE_FLASH_PAGE_Command {
|
|
uint32_t target_addr;
|
|
uint32_t data[flash_page_size];
|
|
};
|
|
*/
|
|
// no result
|
|
|
|
export const HF2_CMD_CHKSUM_PAGES = 0x0007
|
|
/*
|
|
struct HF2_CHKSUM_PAGES_Command {
|
|
uint32_t target_addr;
|
|
uint32_t num_pages;
|
|
};
|
|
struct HF2_CHKSUM_PAGES_Result {
|
|
uint16_t chksums[num_pages];
|
|
};
|
|
*/
|
|
|
|
export const HF2_CMD_READ_WORDS = 0x0008
|
|
/*
|
|
struct HF2_READ_WORDS_Command {
|
|
uint32_t target_addr;
|
|
uint32_t num_words;
|
|
};
|
|
struct HF2_READ_WORDS_Result {
|
|
uint32_t words[num_words];
|
|
};
|
|
*/
|
|
|
|
export const HF2_CMD_WRITE_WORDS = 0x0009
|
|
/*
|
|
struct HF2_WRITE_WORDS_Command {
|
|
uint32_t target_addr;
|
|
uint32_t num_words;
|
|
uint32_t words[num_words];
|
|
};
|
|
*/
|
|
// no result
|
|
|
|
export const HF2_CMD_DMESG = 0x0010
|
|
// no arguments
|
|
// results is utf8 character array
|
|
|
|
export const HF2_FLAG_SERIAL_OUT = 0x80
|
|
export const HF2_FLAG_SERIAL_ERR = 0xC0
|
|
export const HF2_FLAG_CMDPKT_LAST = 0x40
|
|
export const HF2_FLAG_CMDPKT_BODY = 0x00
|
|
export const HF2_FLAG_MASK = 0xC0
|
|
export const HF2_SIZE_MASK = 63
|
|
|
|
export const HF2_STATUS_OK = 0x00
|
|
export const HF2_STATUS_INVALID_CMD = 0x01
|
|
export const HF2_STATUS_EXEC_ERR = 0x02
|
|
export const HF2_STATUS_EVENT = 0x80
|
|
|
|
// the eventId is overlayed on the tag+status; the mask corresponds
|
|
// to the HF2_STATUS_EVENT above
|
|
export const HF2_EV_MASK = 0x800000
|
|
|
|
export const HF2_CMD_JDS_CONFIG = 0x0020
|
|
export const HF2_CMD_JDS_SEND = 0x0021
|
|
export const HF2_EV_JDS_PACKET = 0x800020
|
|
|
|
|
|
export class Transport {
|
|
dev: USBDevice;
|
|
iface: USBInterface;
|
|
altIface: USBAlternateInterface;
|
|
epIn: USBEndpoint;
|
|
epOut: USBEndpoint;
|
|
readLoopStarted = false;
|
|
ready = false;
|
|
|
|
onData = (v: Uint8Array) => { };
|
|
onError = (e: Error) => {
|
|
console.error("HF2 error: " + (e ? e.stack : e))
|
|
};
|
|
|
|
log(msg: string, v?: any) {
|
|
if (v != undefined)
|
|
console.log("HF2: " + msg, v)
|
|
else
|
|
console.log("HF2: " + msg)
|
|
}
|
|
|
|
|
|
private clearDev() {
|
|
if (this.dev) {
|
|
this.dev = null
|
|
this.epIn = null
|
|
this.epOut = null
|
|
}
|
|
}
|
|
|
|
disconnectAsync() {
|
|
this.ready = false
|
|
if (!this.dev) return Promise.resolve()
|
|
this.log("close device")
|
|
return this.dev.close()
|
|
.catch(e => {
|
|
// just ignore errors closing, most likely device just disconnected
|
|
})
|
|
.then(() => {
|
|
this.clearDev()
|
|
return U.delay(500)
|
|
})
|
|
}
|
|
|
|
private recvPacketAsync(): Promise<Uint8Array> {
|
|
let final = (res: USBInTransferResult) => {
|
|
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)
|
|
}
|
|
|
|
error(msg: string) {
|
|
throw new Error(`USB error on device ${this.dev ? this.dev.productName : "n/a"} (${msg})`)
|
|
}
|
|
|
|
private async readLoop() {
|
|
if (this.readLoopStarted)
|
|
return
|
|
this.readLoopStarted = true
|
|
this.log("start read loop")
|
|
|
|
while (true) {
|
|
if (!this.ready) {
|
|
break
|
|
//await U.delay(300)
|
|
//continue
|
|
}
|
|
|
|
try {
|
|
const buf = await this.recvPacketAsync()
|
|
|
|
if (buf[0]) {
|
|
// we've got data; retry reading immedietly after processing it
|
|
this.onData(buf)
|
|
} else {
|
|
// throttle down if no data coming
|
|
await U.delay(5)
|
|
}
|
|
} catch (err) {
|
|
if (this.dev)
|
|
this.onError(err)
|
|
await U.delay(300)
|
|
}
|
|
}
|
|
}
|
|
|
|
sendPacketAsync(pkt: Uint8Array) {
|
|
if (!this.dev)
|
|
return Promise.reject(new Error("Disconnected"))
|
|
U.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")
|
|
})
|
|
}
|
|
|
|
|
|
async init() {
|
|
const usb = new webusb.USB({
|
|
devicesFound: async devices => {
|
|
for (const device of devices) {
|
|
if (device.deviceVersionMajor == 42) {
|
|
for (const iface of device.configuration.interfaces) {
|
|
const alt = iface.alternates[0]
|
|
if (alt.interfaceClass == 0xff && alt.interfaceSubclass == 42) {
|
|
this.dev = device
|
|
this.iface = iface
|
|
this.altIface = alt
|
|
return device
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
})
|
|
|
|
this.dev = await usb.requestDevice({ filters: [{}] })
|
|
this.log("connect device: " + this.dev.manufacturerName + " " + this.dev.productName)
|
|
|
|
await this.dev.open()
|
|
await this.dev.selectConfiguration(1)
|
|
|
|
if (this.altIface.endpoints.length) {
|
|
this.epIn = this.altIface.endpoints.filter(e => e.direction == "in")[0]
|
|
this.epOut = this.altIface.endpoints.filter(e => e.direction == "out")[0]
|
|
U.assert(this.epIn.packetSize == 64);
|
|
U.assert(this.epOut.packetSize == 64);
|
|
}
|
|
this.log("claim interface")
|
|
await this.dev.claimInterface(this.iface.interfaceNumber)
|
|
this.log("all connected")
|
|
this.ready = true
|
|
this.readLoop()
|
|
}
|
|
}
|
|
|
|
export class Proto {
|
|
eventHandlers: U.SMap<(buf: Uint8Array) => void> = {}
|
|
msgs = new U.PromiseBuffer<Uint8Array>()
|
|
cmdSeq = (Math.random() * 0xffff) | 0;
|
|
private lock = new U.PromiseQueue();
|
|
|
|
constructor(public io: Transport) {
|
|
let frames: Uint8Array[] = []
|
|
|
|
io.onData = buf => {
|
|
let tp = buf[0] & HF2_FLAG_MASK
|
|
let len = buf[0] & 63
|
|
//console.log(`msg tp=${tp} len=${len}`)
|
|
let frame = new Uint8Array(len)
|
|
U.memcpy(frame, 0, buf, 1, len)
|
|
if (tp & HF2_FLAG_SERIAL_OUT) {
|
|
this.onSerial(frame, tp == HF2_FLAG_SERIAL_ERR)
|
|
return
|
|
}
|
|
frames.push(frame)
|
|
if (tp == HF2_FLAG_CMDPKT_BODY) {
|
|
return
|
|
} else {
|
|
U.assert(tp == HF2_FLAG_CMDPKT_LAST)
|
|
let total = 0
|
|
for (let f of frames) total += f.length
|
|
let r = new Uint8Array(total)
|
|
let ptr = 0
|
|
for (let f of frames) {
|
|
U.memcpy(r, ptr, f)
|
|
ptr += f.length
|
|
}
|
|
frames = []
|
|
if (r[2] & HF2_STATUS_EVENT) {
|
|
// asynchronous event
|
|
this.handleEvent(r)
|
|
} else {
|
|
this.msgs.push(r)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
error(m: string) {
|
|
return this.io.error(m)
|
|
}
|
|
|
|
talkAsync(cmd: number, data?: Uint8Array) {
|
|
let len = 8
|
|
if (data) len += data.length
|
|
let pkt = new Uint8Array(len)
|
|
let seq = ++this.cmdSeq & 0xffff
|
|
U.write32(pkt, 0, cmd);
|
|
U.write16(pkt, 4, seq);
|
|
U.write16(pkt, 6, 0);
|
|
if (data)
|
|
U.memcpy(pkt, 8, data, 0, data.length)
|
|
let numSkipped = 0
|
|
let handleReturnAsync = (): Promise<Uint8Array> =>
|
|
this.msgs.shiftAsync(1000) // we wait up to a second
|
|
.then(res => {
|
|
if (U.read16(res, 0) != seq) {
|
|
if (numSkipped < 3) {
|
|
numSkipped++
|
|
this.io.log(`message out of sync, (${seq} vs ${U.read16(res, 0)}); will re-try`)
|
|
return handleReturnAsync()
|
|
}
|
|
this.error("out of sync")
|
|
}
|
|
let info = ""
|
|
if (res[3])
|
|
info = "; info=" + res[3]
|
|
switch (res[2]) {
|
|
case HF2_STATUS_OK:
|
|
return res.slice(4)
|
|
case HF2_STATUS_INVALID_CMD:
|
|
this.error("invalid command" + info)
|
|
break
|
|
case HF2_STATUS_EXEC_ERR:
|
|
this.error("execution error" + info)
|
|
break
|
|
default:
|
|
this.error("error " + res[2] + info)
|
|
break
|
|
}
|
|
return null
|
|
})
|
|
|
|
return this.lock.enqueue("talk", () =>
|
|
this.sendMsgAsync(pkt)
|
|
.then(handleReturnAsync))
|
|
}
|
|
|
|
|
|
private sendMsgAsync(buf: Uint8Array, serial: number = 0) {
|
|
// Util.assert(buf.length <= this.maxMsgSize)
|
|
let frame = new Uint8Array(64)
|
|
let loop = (pos: number): Promise<void> => {
|
|
let len = buf.length - pos
|
|
if (len <= 0) return Promise.resolve()
|
|
if (len > 63) {
|
|
len = 63
|
|
frame[0] = HF2_FLAG_CMDPKT_BODY;
|
|
} else {
|
|
frame[0] = HF2_FLAG_CMDPKT_LAST;
|
|
}
|
|
if (serial) frame[0] = serial == 1 ? HF2_FLAG_SERIAL_OUT : HF2_FLAG_SERIAL_ERR;
|
|
frame[0] |= len;
|
|
for (let i = 0; i < len; ++i)
|
|
frame[i + 1] = buf[pos + i]
|
|
return this.io.sendPacketAsync(frame)
|
|
.then(() => loop(pos + len))
|
|
}
|
|
return loop(0)
|
|
}
|
|
|
|
onEvent(id: number, f: (buf: Uint8Array) => void) {
|
|
U.assert(!!(id & HF2_EV_MASK))
|
|
this.eventHandlers[id + ""] = f
|
|
}
|
|
|
|
onJDMessage(f: (buf: Uint8Array) => void) {
|
|
this.talkAsync(HF2_CMD_JDS_CONFIG, U.encodeU32LE([1]))
|
|
this.onEvent(HF2_EV_JDS_PACKET, f)
|
|
}
|
|
|
|
sendJDMessageAsync(buf: Uint8Array) {
|
|
return this.talkAsync(HF2_CMD_JDS_SEND, buf)
|
|
}
|
|
|
|
handleEvent(buf: Uint8Array) {
|
|
let evid = U.read32(buf, 0)
|
|
let f = this.eventHandlers[evid + ""]
|
|
if (f) {
|
|
f(buf.slice(4))
|
|
} else {
|
|
this.io.log("unhandled event: " + evid.toString(16))
|
|
}
|
|
}
|
|
onSerial(data: Uint8Array, iserr: boolean) {
|
|
console.log("SERIAL:", U.bufferToString(data))
|
|
}
|
|
|
|
async init() {
|
|
await this.io.init()
|
|
const buf = await this.talkAsync(HF2_CMD_INFO)
|
|
this.io.log("Connected to: " + U.bufferToString(buf))
|
|
}
|
|
}
|