| 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; |
| } |