diff --git a/src/old/Connection.tsx b/src/old/Connection.tsx deleted file mode 100644 index a02a1628..00000000 --- a/src/old/Connection.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React from 'react'; - -const pybricksServiceUUID = 'c5f50001-8280-46da-89f4-6d8051e4aeef'; -const bleNusServiceUUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'; -const bleNusCharRXUUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'; -const bleNusCharTXUUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'; - -enum connectionState { - disconnected, - connecting, - connected, - disconnecting, -} - -function connectionStateToButtonText(state: connectionState): string { - switch (state) { - case connectionState.disconnected: - return 'Connect'; - case connectionState.connecting: - return 'Connecting...'; - case connectionState.connected: - return 'Disconnect'; - case connectionState.disconnecting: - return 'Disconnecting...'; - default: - return 'Error'; - } -} - -type ConnectionProperties = { - onData: (data: Uint8Array) => void; -}; -type ConnectionState = { - connection: connectionState; -}; - -class Connection extends React.Component { - private device?: BluetoothDevice; - private rxChar?: BluetoothRemoteGATTCharacteristic; - - constructor(props: ConnectionProperties) { - super(props); - this.state = { connection: connectionState.disconnected }; - this.onConnectClicked = this.onConnectClicked.bind(this); - } - - public write(data: BufferSource): Promise { - return this.rxChar?.writeValue(data) ?? Promise.reject('No rxChar'); - } - - public async downloadAndRun(data: Uint8Array): Promise { - const sizeData = new Uint8Array(4); - new DataView(sizeData.buffer).setUint32(0, data.length); - await this.write(sizeData); - const chunk = 20; - for (let i = 0; i < data.length; i += chunk) { - await this.write(data.slice(i, i + chunk)); - // TODO: wait for checksum response on every 5th write - } - } - - private async connect(): Promise { - try { - if (this.device !== undefined) { - throw Error('Already connected.'); - } - if (navigator.bluetooth === undefined) { - // TODO: custom exception type - throw Error( - 'WebBluetooth API is not available. Please make sure the Web Bluetooth flag is enabled.', - ); - } - this.device = await navigator.bluetooth.requestDevice({ - filters: [{ services: [pybricksServiceUUID] }], - optionalServices: [bleNusServiceUUID], - }); - if (this.device.gatt === undefined) { - throw Error('Device does not support GATT'); - } - this.device.addEventListener('gattserverdisconnected', () => - this.setState({ connection: connectionState.disconnected }), - ); - } catch (err) { - this.setState({ connection: connectionState.disconnected }); - throw err; - } - const server = await this.device.gatt.connect(); - const service = await server.getPrimaryService(bleNusServiceUUID); - this.rxChar = await service.getCharacteristic(bleNusCharRXUUID); - const txChar = await service.getCharacteristic(bleNusCharTXUUID); - txChar.addEventListener('characteristicvaluechanged', () => { - if (!txChar.value) { - return; - } - this.props.onData(new Uint8Array(txChar.value.buffer)); - }); - await txChar.startNotifications(); - this.setState({ connection: connectionState.connected }); - } - - private disconnect(): void { - if (this.device !== undefined) { - this.device.gatt?.disconnect(); - this.device = undefined; - this.rxChar = undefined; - } - } - - private async onConnectClicked(): Promise { - if (this.state.connection === connectionState.disconnected) { - this.setState({ connection: connectionState.connecting }); - try { - await this.connect(); - } catch (err) { - // FIXME: need proper error dialog - alert(err); - } - } else { - this.setState({ connection: connectionState.disconnecting }); - try { - this.disconnect(); - } catch (err) { - // FIXME: need proper error dialog - alert(err); - } - } - } - - render(): JSX.Element { - return ( - - ); - } -} - -export { Connection }; diff --git a/src/old/Flash.tsx b/src/old/Flash.tsx deleted file mode 100644 index 5c19ae91..00000000 --- a/src/old/Flash.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import React from 'react'; -import Dropzone from 'react-dropzone'; - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -const legoFirmwareServiceUUID = '00001625-1212-efde-1623-785feabcd123'; -const legoFirmwareCharUUID = '00001626-1212-efde-1623-785feabcd123'; - -enum legoFirmwareCmd { - eraseFlash = 0x11, - programFlash = 0x22, - startApp = 0x33, - initLoader = 0x44, - getInfo = 0x55, - getChecksum = 0x66, - getFlashState = 0x77, - disconnect = 0x88, -} - -enum legoHubType { - moveHub = 0x40, - cityHub = 0x41, - cplusHub = 0x80, -} - -const legoFirmwareError = 0x05; - -function createEraseFlashRequest(): Uint8Array { - const msg = new Uint8Array(1); - const view = new DataView(msg.buffer); - view.setUint8(0, legoFirmwareCmd.eraseFlash); - return msg; -} - -function createProgramFlashRequest(address: number, payload: ArrayBuffer): Uint8Array { - const size = payload.byteLength; - if (size > 14) { - throw Error('size too big'); - } - const msg = new Uint8Array(size + 6); - const view = new DataView(msg.buffer); - view.setUint8(0, legoFirmwareCmd.programFlash); - view.setUint8(1, size + 4); - view.setUint32(2, address, true); - const payloadView = new DataView(payload); - for (let i = 0; i < size; i++) { - view.setUint8(6 + i, payloadView.getUint8(i)); - } - return msg; -} - -function createStartAppRequest(): Uint8Array { - const msg = new Uint8Array(1); - const view = new DataView(msg.buffer); - view.setUint8(0, legoFirmwareCmd.startApp); - return msg; -} - -function createInitLoaderRequest(fwSize: number): Uint8Array { - const msg = new Uint8Array(5); - const view = new DataView(msg.buffer); - view.setUint8(0, legoFirmwareCmd.initLoader); - view.setUint32(1, fwSize, true); - return msg; -} - -function createGetInfoRequest(): Uint8Array { - const msg = new Uint8Array(1); - const view = new DataView(msg.buffer); - view.setUint8(0, legoFirmwareCmd.getInfo); - return msg; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function createGetChecksumRequest(): Uint8Array { - const msg = new Uint8Array(1); - const view = new DataView(msg.buffer); - view.setUint8(0, legoFirmwareCmd.getChecksum); - return msg; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function createGetFlashStateRequest(): Uint8Array { - const msg = new Uint8Array(1); - const view = new DataView(msg.buffer); - view.setUint8(0, legoFirmwareCmd.getFlashState); - return msg; -} - -function assertError(msg: DataView): void { - if (msg.getUint8(0) === legoFirmwareError && msg.getUint8(1) === 5) { - throw Error( - `msg type: 0x${msg.getUint8(3).toString(16)} error: 0x${msg - .getUint8(4) - .toString(16)}`, - ); - } -} - -function parseEraseFlashReply(msg: DataView): { result: number } { - assertError(msg); - if (msg.getUint8(0) !== legoFirmwareCmd.eraseFlash) { - throw Error('expecting erase flash command'); - } - return { result: msg.getUint8(1) }; -} - -function parseProgramFlashReply(msg: DataView): { checksum: number; count: number } { - assertError(msg); - if (msg.getUint8(0) !== legoFirmwareCmd.programFlash) { - throw Error('expecting program flash command'); - } - return { checksum: msg.getUint8(1), count: msg.getUint32(2, true) }; -} - -function parseInitLoaderReply(msg: DataView): { result: number } { - assertError(msg); - if (msg.getUint8(0) !== legoFirmwareCmd.initLoader) { - throw Error('expecting init loader command'); - } - return { result: msg.getUint8(1) }; -} - -function parseGetInfoReply( - msg: DataView, -): { version: number; startAddress: number; endAddress: number; typeId: legoHubType } { - assertError(msg); - if (msg.getUint8(0) !== legoFirmwareCmd.getInfo) { - throw Error('expecting get info command'); - } - return { - version: msg.getUint32(1, true), - startAddress: msg.getUint32(5, true), - endAddress: msg.getUint32(9, true), - typeId: msg.getUint8(13), - }; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function parseGetChecksumReply(msg: DataView): { checksum: number } { - assertError(msg); - if (msg.getUint8(0) !== legoFirmwareCmd.getChecksum) { - throw Error('expecting get checksum command'); - } - return { checksum: msg.getUint8(1) }; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function parseGetFlashStateReply(msg: DataView): { level: number } { - assertError(msg); - if (msg.getUint8(0) !== legoFirmwareCmd.getFlashState) { - throw Error('expecting get flash state command'); - } - return { level: msg.getUint8(1) }; -} - -function getResponse( - char: BluetoothRemoteGATTCharacteristic, - decode: (msg: DataView) => T, - timeout = 100, -): Promise { - let handler: EventListener; - return new Promise((resolve, reject) => { - handler = (): void => { - if (char.value === undefined) { - reject('unexpected undefined value'); - } else { - resolve(decode(char.value)); - } - }; - char.addEventListener('characteristicvaluechanged', handler); - setTimeout(() => reject('timed out'), timeout); - }).finally(() => { - char.removeEventListener('characteristicvaluechanged', handler); - }); -} - -async function sendRequest( - char: BluetoothRemoteGATTCharacteristic, - msg: BufferSource, -): Promise { - return char.writeValue(msg); -} - -class Flash extends React.Component { - constructor(props: {}) { - super(props); - this.onDropAccepted = this.onDropAccepted.bind(this); - this.onDropRejected = this.onDropRejected.bind(this); - } - - private async flash(data: ArrayBuffer): Promise { - if (navigator.bluetooth === undefined) { - throw Error('No web bluetooth'); - } - const device = await navigator.bluetooth.requestDevice({ - filters: [{ services: [legoFirmwareServiceUUID] }], - }); - if (device.gatt === undefined) { - throw Error('Device does not support GATT'); - } - device.addEventListener('gattserverdisconnected', () => { - // this.setState({ connection: connectionState.disconnected }); - }); - const server = await device.gatt.connect(); - const service = await server.getPrimaryService(legoFirmwareServiceUUID); - const char = await service.getCharacteristic(legoFirmwareCharUUID); - char.addEventListener('characteristicvaluechanged', () => { - if (!char.value) { - return; - } - // this.props.onData(new Uint8Array(char.value.buffer)); - }); - await char.startNotifications(); - - console.log('Getting info'); - await sendRequest(char, createGetInfoRequest()); - const info = await getResponse(char, parseGetInfoReply); - console.log( - `version: ${info.version.toString( - 16, - )} startAddress: ${info.startAddress.toString( - 16, - )} endAddress: ${info.endAddress.toString( - 16, - )} typeId: ${info.typeId.toString(16)}`, - ); - // TODO: verify typeId === firmware.typeId - - console.log('Erasing flash'); - await sendRequest(char, createEraseFlashRequest()); - const eraseResult = await getResponse(char, parseEraseFlashReply, 5000); - if (eraseResult.result) { - throw Error('Failed to erase'); - } - - console.log('Initializing'); - await sendRequest(char, createInitLoaderRequest(data.byteLength)); - const initResult = await getResponse(char, parseInitLoaderReply); - if (initResult.result) { - throw Error('Failed to init'); - } - - // TODO: we can receive an async error message during this loop - // in which case it would be better to abort early rather than waiting - // for all messages to be sent - for (let offset = 0; offset < data.byteLength; offset += 14) { - console.log(`sending ${offset / 14 + 1} of ${data.byteLength / 14}`); - const payload = data.slice(offset, offset + 14); - await sendRequest( - char, - createProgramFlashRequest(info.startAddress + offset, payload), - ); - - // unfortunately there is not a way to get backpressure from BLE - // so we just have to add a delay to avoid buffer overrun on the - // remote device and it has to be slow enough to work in the worst - // conditions - await sleep(10); - } - - console.log('waiting for confirmation'); - const flashResult = await getResponse(char, parseProgramFlashReply, 5000); - if (flashResult.count !== data.byteLength) { - throw Error("Didn't flash all bytes"); - } - - // this will cause the remote device to disconnect and reboot - console.log('restarting'); - await sendRequest(char, createStartAppRequest()); - } - - private onDropAccepted(acceptedFiles: File[]): void { - // should only be one file since multiple={false} - acceptedFiles.forEach((f) => { - const reader = new FileReader(); - - reader.onabort = (): void => console.log('file reading was aborted'); - reader.onerror = (): void => console.log('file reading has failed'); - reader.onload = (): void => { - // Do whatever you want with the file contents - const binaryStr = reader.result; - if (binaryStr === null) { - throw Error('Unexpected null binaryStr'); - } - if (typeof binaryStr === 'string') { - throw Error('Unexpected string binaryStr'); - } - this.flash(binaryStr); - }; - reader.readAsArrayBuffer(f); - }); - } - - private onDropRejected(rejectedFiles: File[]): void { - // should only be one file since multiple={false} - rejectedFiles.forEach((f) => { - alert(`bad file ${f.name}`); - }); - } - - render(): JSX.Element { - return ( - - {({ getRootProps, getInputProps }): JSX.Element => ( -
-
- -

- Drag and drop a firmware file here, or click to select a - file -

-
-
- )} -
- ); - } -} - -export { Flash }; diff --git a/src/old/Repl.tsx b/src/old/Repl.tsx deleted file mode 100644 index 9cd2fb65..00000000 --- a/src/old/Repl.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { Connection } from './Connection'; - -interface ReplProperties { - readonly connection: React.RefObject; -} - -class Repl extends React.Component { - constructor(props: ReplProperties) { - super(props); - this.onClick = this.onClick.bind(this); - } - private async onClick(): Promise { - try { - // send 4 space characters - await this.props.connection.current?.write( - new Uint8Array([0x20, 0x20, 0x20, 0x20]), - ); - } catch (err) { - alert(err); - } - } - - render(): JSX.Element { - return ( - - ); - } -} - -export { Repl }; diff --git a/src/old/Run.tsx b/src/old/Run.tsx deleted file mode 100644 index 2461e326..00000000 --- a/src/old/Run.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import AceEditor from 'react-ace'; -import MpyCross from '@pybricks/mpy-cross'; -import { Connection } from './Connection'; - -const mpy = MpyCross({ arguments: ['-mno-unicode'] }); - -interface RunProperties { - readonly editor: React.RefObject; - readonly connection: React.RefObject; -} - -class Run extends React.Component { - constructor(props: RunProperties) { - super(props); - this.onClick = this.onClick.bind(this); - } - private async onClick(): Promise { - try { - const session = this.props.editor.current?.editor.getSession(); - const document = session.getDocument(); - const data = mpy.compile(document.getValue()); - await this.props.connection.current?.downloadAndRun(data); - } catch (err) { - alert(err); - } - } - - render(): JSX.Element { - return ( - - ); - } -} - -export { Run }; diff --git a/src/old/Stop.tsx b/src/old/Stop.tsx deleted file mode 100644 index 850e4303..00000000 --- a/src/old/Stop.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { Connection } from './Connection'; - -interface StopProperties { - readonly connection: React.RefObject; -} - -class Stop extends React.Component { - constructor(props: StopProperties) { - super(props); - this.onClick = this.onClick.bind(this); - } - private async onClick(): Promise { - try { - // send CTRL+C, CTRL+C, CTRL+D - await this.props.connection.current?.write( - new Uint8Array([0x03, 0x03, 0x04]), - ); - } catch (err) { - alert(err); - } - } - - render(): JSX.Element { - return ( - - ); - } -} - -export { Stop };