| function bufferToHex(buffer) { | |
| return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join(''); | |
| } | |
| class PacketSource { | |
| constructor(pyodide) { | |
| this.parser = pyodide.runPython(` | |
| from bumble.transport.common import PacketParser | |
| class ProxiedPacketParser(PacketParser): | |
| def feed_data(self, js_data): | |
| super().feed_data(bytes(js_data.to_py())) | |
| ProxiedPacketParser() | |
| `); | |
| } | |
| set_packet_sink(sink) { | |
| this.parser.set_packet_sink(sink); | |
| } | |
| data_received(data) { | |
| //console.log(`HCI[controller->host]: ${bufferToHex(data)}`); | |
| this.parser.feed_data(data); | |
| } | |
| } | |
| class PacketSink { | |
| constructor() { | |
| this.queue = []; | |
| this.isProcessing = false; | |
| } | |
| on_packet(packet) { | |
| if (!this.writer) { | |
| return; | |
| } | |
| const buffer = packet.toJs({create_proxies : false}); | |
| packet.destroy(); | |
| //console.log(`HCI[host->controller]: ${bufferToHex(buffer)}`); | |
| this.queue.push(buffer); | |
| this.processQueue(); | |
| } | |
| async processQueue() { | |
| if (this.isProcessing) { | |
| return; | |
| } | |
| this.isProcessing = true; | |
| while (this.queue.length > 0) { | |
| const buffer = this.queue.shift(); | |
| await this.writer(buffer); | |
| } | |
| this.isProcessing = false; | |
| } | |
| } | |
| class LogEvent extends Event { | |
| constructor(message) { | |
| super('log'); | |
| this.message = message; | |
| } | |
| } | |
| export class Bumble extends EventTarget { | |
| constructor(pyodide) { | |
| super(); | |
| this.pyodide = pyodide; | |
| } | |
| async loadRuntime(bumblePackage) { | |
| // Load pyodide if it isn't provided. | |
| if (this.pyodide === undefined) { | |
| this.log('Loading Pyodide'); | |
| this.pyodide = await loadPyodide(); | |
| } | |
| // Load the Bumble module | |
| console.log('Installing micropip'); | |
| this.log(`Installing ${bumblePackage}`) | |
| await this.pyodide.loadPackage('micropip'); | |
| await this.pyodide.runPythonAsync(` | |
| import micropip | |
| await micropip.install('${bumblePackage}') | |
| package_list = micropip.list() | |
| print(package_list) | |
| `) | |
| // Mount a filesystem so that we can persist data like the Key Store | |
| let mountDir = '/bumble'; | |
| this.pyodide.FS.mkdir(mountDir); | |
| this.pyodide.FS.mount(this.pyodide.FS.filesystems.IDBFS, { root: '.' }, mountDir); | |
| // Sync previously persisted filesystem data into memory | |
| await new Promise(resolve => { | |
| this.pyodide.FS.syncfs(true, () => { | |
| console.log('FS synced in'); | |
| resolve(); | |
| }); | |
| }) | |
| // Setup the HCI source and sink | |
| this.packetSource = new PacketSource(this.pyodide); | |
| this.packetSink = new PacketSink(); | |
| } | |
| log(message) { | |
| this.dispatchEvent(new LogEvent(message)); | |
| } | |
| async connectWebSocketTransport(hciWsUrl) { | |
| return new Promise((resolve, reject) => { | |
| let resolved = false; | |
| let ws = new WebSocket(hciWsUrl); | |
| ws.binaryType = 'arraybuffer'; | |
| ws.onopen = () => { | |
| this.log('WebSocket open'); | |
| resolve(); | |
| resolved = true; | |
| } | |
| ws.onclose = () => { | |
| this.log('WebSocket close'); | |
| if (!resolved) { | |
| reject(`Failed to connect to ${hciWsUrl}`); | |
| } | |
| } | |
| ws.onmessage = (event) => { | |
| this.packetSource.data_received(event.data); | |
| } | |
| this.packetSink.writer = (packet) => { | |
| if (ws.readyState === WebSocket.OPEN) { | |
| ws.send(packet); | |
| } | |
| } | |
| this.closeTransport = async () => { | |
| if (ws.readyState === WebSocket.OPEN) { | |
| ws.close(); | |
| } | |
| } | |
| }) | |
| } | |
| async loadApp(appUrl) { | |
| this.log('Loading app'); | |
| const script = await (await fetch(appUrl)).text(); | |
| await this.pyodide.runPythonAsync(script); | |
| const pythonMain = this.pyodide.globals.get('main'); | |
| const app = await pythonMain(this.packetSource, this.packetSink); | |
| if (app.on) { | |
| app.on('key_store_update', this.onKeystoreUpdate.bind(this)); | |
| } | |
| this.log('App is ready!'); | |
| return app; | |
| } | |
| onKeystoreUpdate() { | |
| // Sync the FS | |
| this.pyodide.FS.syncfs(() => { | |
| console.log('FS synced out'); | |
| }); | |
| } | |
| } | |
| async function getBumblePackage() { | |
| const params = (new URL(document.location)).searchParams; | |
| // First check the packageFile override param | |
| if (params.has('packageFile')) { | |
| return await (await fetch('/packageFile')).text() | |
| } | |
| // Then check the package override param | |
| if (params.has('package')) { | |
| return params.get('package') | |
| } | |
| // If no override params, default to the main package | |
| return 'bumble' | |
| } | |
| export async function setupSimpleApp(appUrl, bumbleControls, log) { | |
| // Load Bumble | |
| log('Loading Bumble'); | |
| const bumble = new Bumble(); | |
| bumble.addEventListener('log', (event) => { | |
| log(event.message); | |
| }) | |
| await bumble.loadRuntime(await getBumblePackage()); | |
| log('Bumble is ready!') | |
| const app = await bumble.loadApp(appUrl); | |
| bumbleControls.connector = async (hciWsUrl) => { | |
| try { | |
| // Connect the WebSocket HCI transport | |
| await bumble.connectWebSocketTransport(hciWsUrl); | |
| // Start the app | |
| await app.start(); | |
| return true; | |
| } catch (err) { | |
| log(err); | |
| return false; | |
| } | |
| } | |
| bumbleControls.stopper = async () => { | |
| // Stop the app | |
| await app.stop(); | |
| // Close the HCI transport | |
| await bumble.closeTransport(); | |
| } | |
| bumbleControls.onBumbleLoaded(); | |
| return app; | |
| } |