From 15c8908eca51eda4447eeff0fadc385464253e5c Mon Sep 17 00:00:00 2001 From: Charly-sketch Date: Mon, 7 Oct 2024 09:53:40 +0000 Subject: [PATCH] feat: :sparkles: Add WebUSB editor extension for STEAMI Board Able to compile Able to flash the card For now only with local serve --- pxt-steami/editor/extension.ts | 158 +++++++++++++++++- pxt-steami/editor/stm_dap_flash.ts | 246 +++++++++++++++++++++++++++++ 2 files changed, 396 insertions(+), 8 deletions(-) create mode 100644 pxt-steami/editor/stm_dap_flash.ts diff --git a/pxt-steami/editor/extension.ts b/pxt-steami/editor/extension.ts index bcc3fdf..d27cfd8 100644 --- a/pxt-steami/editor/extension.ts +++ b/pxt-steami/editor/extension.ts @@ -1,13 +1,155 @@ -/// -/// -/// -/// +/// +/// +/// +/// +//import * as dialogs from "./dialogs"; +import * as flash from './stm_dap_flash'; +//import * as patch from "./patch"; + +pxt.editor.initExtensionsAsync = function ( + opts: pxt.editor.ExtensionOptions, +): Promise { + pxt.debug('loading STM target extensions...'); + console.log('loading STM target extensions...'); + + let body = document.getElementsByTagName('body')[0]; + let container = document.createElement('div'); + let content = document.createElement('div'); + + container.setAttribute('id', 'upload_modal_container'); + container.style.backgroundColor = 'rgba(20, 20, 20, 0.7)'; + container.style.position = 'absolute'; + container.style.top = '0'; + container.style.left = '0'; + container.style.width = '100vw'; + container.style.height = '100vh'; + container.style.zIndex = '99999'; + container.style.display = 'none'; + + content.style.zIndex = '999999'; + content.style.width = '25vw'; + content.style.position = 'relative'; + content.style.top = '50%'; + content.style.left = '50%'; + content.style.transform = 'translate(-50%, -50%)'; + content.style.borderRadius = '5px'; + content.style.overflow = 'hidden'; + content.innerHTML = `
+

Uploading

+
+
+

+ Your program is uploading to your target, please wait.

+ Do not unplug your board, do not close this tab nor change tab during uploading.
+

+
+

0%

+
+
+ + +
`; + + container.appendChild(content); + body.appendChild(container); + + const manyAny = Math as any; + if (!manyAny.imul) + manyAny.imul = function (a: number, b: number): number { + const ah = (a >>> 16) & 0xffff; + const al = a & 0xffff; + const bh = (b >>> 16) & 0xffff; + const bl = b & 0xffff; + // 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; + }; -pxt.editor.initExtensionsAsync = function (opts: pxt.editor.ExtensionOptions): Promise { - pxt.debug('loading STM target extensions...') - console.log('loading STM target extensions...') const res: pxt.editor.ExtensionResult = { + hexFileImporters: [], + }; + + res.deployAsync = async function (r: pxtc.CompileResult): Promise { + var wrapper = (await pxt.packetio.initAsync()) as flash.STMDAPWrapper; + + if (!r.success) { + return Promise.reject(); + } + + if (wrapper.isTargetReady() && !r.saveOnly) { + wrapper.onFlashFinish = error => { + wrapper.onFlashProgress = null; + wrapper.onFlashFinish = null; + + if (error == null) { + document.getElementById( + 'upload_modal_message', + ).innerHTML = `Upload complete !`; + } else if (error instanceof Error) { + document.getElementById( + 'upload_modal_message', + ).innerHTML = `Upload failed !
Reason: [${error.name}] ${error.message}

Try unplugging your card and then plugging it back in.`; + } else { + document.getElementById( + 'upload_modal_message', + ).innerHTML = `Upload failed !
Reason: ${error}

Try unplugging your card and then plugging it back in.`; + } + + document.getElementById('upload_modal_button').style.display = + 'block'; + document.getElementById('upload_modal_message').style.display = + 'block'; + }; + + wrapper.onFlashProgress = prg => { + let bar = document.getElementById('upload_modal_bar'); + let text = document.getElementById('upload_modal_value'); + + text.innerText = Math.round(prg * 100) + '%'; + bar.style.width = `${prg * 100}%`; + }; + + document.getElementById('upload_modal_container').style.display = + 'block'; + document.getElementById('upload_modal_message').style.display = + 'none'; + document.getElementById('upload_modal_message').innerText = ''; + document.getElementById('upload_modal_button').style.display = + 'none'; + wrapper.onFlashProgress(0); + + return wrapper.reflashAsync(r).catch(() => { + console.error('Failed to upload...'); + return pxt.commands.saveOnlyAsync(r); + }); + } else { + console.log('Target not ready or save only !'); + return pxt.commands.saveOnlyAsync(r); + } }; + + pxt.usb.setFilters([ + { + vendorId: 0x0d28, + productId: 0x0204, + classCode: 0xff, + subclassCode: 0x03, // the ctrl pipe endpoint + }, + { + vendorId: 0x0d28, + productId: 0x0204, + classCode: 0xff, + subclassCode: 0x00, // the custom CMSIS2 endpoint + }, + ]); + + res.mkPacketIOWrapper = flash.mkSTMDAPPacketIOWrapper; + // res.blocklyPatch = patch.patchBlocks; + // res.renderBrowserDownloadInstructions = dialogs.renderBrowserDownloadInstructions; + // res.renderUsbPairDialog = dialogs.renderUsbPairDialog; return Promise.resolve(res); -} \ No newline at end of file +}; diff --git a/pxt-steami/editor/stm_dap_flash.ts b/pxt-steami/editor/stm_dap_flash.ts new file mode 100644 index 0000000..dd58399 --- /dev/null +++ b/pxt-steami/editor/stm_dap_flash.ts @@ -0,0 +1,246 @@ +import * as DAPjs from 'dapjs'; + +const SERIAL_BAUDRATE = 115200; +const PERIOD_SERIAL_SEND_MS = 500; +const HEX_FILENAME = 'binary.hex'; + +function log(msg: string, ...optionsParams: any[]) { + console.log(`STM DAP : ${msg}`, ...optionsParams); +} +function log_error(msg: string, ...optionsParams: any[]) { + console.error(`STM DAP ERROR: ${msg}`, ...optionsParams); +} + +export class STMDAPWrapper implements pxt.packetio.PacketIOWrapper { + private initialized = false; + + familyID: number; + icon = 'usb'; + + public onFlashProgress: (prg: number) => void = null; + public onFlashFinish: (error: any) => void = null; + + private target: DAPjs.DAPLink = null; + private lastSerialPrint: number = 0; + private serialBuffer: string = ''; + private lock_serial = false; + + 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 => { + log('Wrapper On DATA : ' + pxt.Util.toHex(buf)); + }; + } + + onSerial(buf: Uint8Array, isStderr: boolean) { + log(`On Serial : \n\tBuf : '${buf}'\n\tisStderr : ${isStderr}`); + } + + onCustomEvent(type: string, payload: Uint8Array) { + log(`On Custom Event : \n\type : '${type}'\n\payload : ${payload}`); + } + + async reconnectAsync(): Promise { + log('Reconnect'); + this.initialized = false; + await this.io.reconnectAsync(); + await this.initDAP((this.io as any).dev); + await this.startSerial(SERIAL_BAUDRATE); + this.initialized = true; + return Promise.resolve(); + } + + async disconnectAsync(): Promise { + log('Disconnected'); + this.initialized = false; + + if (this.target != null) { + await this.target.disconnect(); + this.stopSerial(); + this.target = null; + } + + this.serialBuffer = ''; + return Promise.resolve(); + } + + isConnected(): boolean { + return this.io.isConnected() && this.initialized; + } + + isConnecting(): boolean { + return ( + this.io.isConnecting() || + (this.io.isConnected() && !this.initialized) + ); + } + + async reflashAsync(resp: pxtc.CompileResult): Promise { + var blob = new Blob([resp.outfiles[HEX_FILENAME]], { + type: 'text/plain', + }); + const fileReader = new FileReader(); + + console.log(resp); + // TODO : Remove useless part of the file to speed up the upload + + fileReader.onloadend = evt => { + return this.flashDevice(evt.target.result); + }; + + fileReader.onprogress = evt => { + log(`Blob progress : ${(evt.loaded / evt.total) * 100.0} %`); + }; + + fileReader.onerror = evt => { + log_error('Failed to load Blob file : ', fileReader.error); + return Promise.reject(); + }; + + fileReader.readAsArrayBuffer(blob); + } + + sendCustomEventAsync(type: string, payload: Uint8Array): Promise { + throw new Error('Method not implemented.'); + } + + isTargetReady(): boolean { + return this.target != null; + } + + private async initDAP(device: USBDevice) { + const transport = new DAPjs.WebUSB(device); + this.target = new DAPjs.DAPLink(transport); + + log('DAP initialized !'); + } + + private async startSerial(baudrateSerial: number) { + //return; + + if (this.lock_serial) { + return; + } + + this.target.on(DAPjs.DAPLink.EVENT_SERIAL_DATA, (data: string) => { + this.serialBuffer += data; + + if (Date.now() - this.lastSerialPrint > PERIOD_SERIAL_SEND_MS) { + this.processSerialLine(Buffer.from(this.serialBuffer)); + this.serialBuffer = ''; + this.lastSerialPrint = Date.now(); + } + }); + + await this.target.connect(); + await this.target.setSerialBaudrate(baudrateSerial); + await this.target.disconnect(); + + this.target + .startSerialRead() + .catch(e => log_error('ERROR startSerial : ', e)); + log('Serial Started'); + } + + private async stopSerial() { + //return; + + this.target.on(DAPjs.DAPLink.EVENT_SERIAL_DATA, (data: string) => {}); + this.target.stopSerialRead(); + + await this.sleep(1000); + + log('Serial Stopped'); + } + + private processSerialLine(line: Uint8Array) { + if (this.onSerial) { + try { + // catch encoding bugs + this.onSerial(line, false); + } catch (err) { + log_error(`serial decoding error: ${err.message}`); + pxt.tickEvent('hid.flash.serial.decode.error'); + log_error('', { err, line }); + } + } + } + + private async flashDevice(buffer: any): Promise { + var errorCatch = null; + log(`Flashing file ${buffer.byteLength} words long`); + + this.target.on(DAPjs.DAPLink.EVENT_PROGRESS, progress => { + if (this.onFlashProgress != null) { + this.onFlashProgress(progress); + } + }); + + try { + pxt.tickEvent('hid.flash.start'); + + log('Stopping Serial'); + this.lock_serial = true; + await this.stopSerial(); + + log('Connect'); + await this.target.connect().catch(e => { + log_error('ERROR connect : ', e); + throw e; + }); + + log('Reset'); + await this.target.reset().catch(e => { + log_error('No reset available on target. Error : ', e); + }); + + log('Flash'); + await this.target.flash(buffer).catch(e => { + log_error('ERROR flash : ', e); + throw e; + }); + + log('Reset'); + await this.target.reset().catch(e => { + log_error('No reset available on target. Error : ', e); + }); + await this.sleep(1000); + + log('Disconnect'); + await this.target.disconnect().catch(e => { + log_error('ERROR disconnect : ', e); + throw e; + }); + } catch (error) { + errorCatch = error; + log_error('Failed to flash : ', error); + return Promise.reject(); + } finally { + this.lock_serial = false; + this.startSerial(SERIAL_BAUDRATE); + if (this.onFlashFinish != null) { + this.onFlashFinish(errorCatch); + } + } + + pxt.tickEvent('hid.flash.success'); + return Promise.resolve(); + } + + private sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} + +export function mkSTMDAPPacketIOWrapper( + io: pxt.packetio.PacketIO, +): pxt.packetio.PacketIOWrapper { + pxt.log(`packetio: mk wrapper STM_dap wrapper`); + return new STMDAPWrapper(io); +}