1function bufferToHex(buffer) { 2 return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join(''); 3} 4 5class PacketSource { 6 constructor(pyodide) { 7 this.parser = pyodide.runPython(` 8 from bumble.transport.common import PacketParser 9 class ProxiedPacketParser(PacketParser): 10 def feed_data(self, js_data): 11 super().feed_data(bytes(js_data.to_py())) 12 ProxiedPacketParser() 13 `); 14 } 15 16 set_packet_sink(sink) { 17 this.parser.set_packet_sink(sink); 18 } 19 20 data_received(data) { 21 //console.log(`HCI[controller->host]: ${bufferToHex(data)}`); 22 this.parser.feed_data(data); 23 } 24} 25 26class PacketSink { 27 constructor() { 28 this.queue = []; 29 this.isProcessing = false; 30 } 31 32 on_packet(packet) { 33 if (!this.writer) { 34 return; 35 } 36 const buffer = packet.toJs({create_proxies : false}); 37 packet.destroy(); 38 //console.log(`HCI[host->controller]: ${bufferToHex(buffer)}`); 39 this.queue.push(buffer); 40 this.processQueue(); 41 } 42 43 async processQueue() { 44 if (this.isProcessing) { 45 return; 46 } 47 this.isProcessing = true; 48 while (this.queue.length > 0) { 49 const buffer = this.queue.shift(); 50 await this.writer(buffer); 51 } 52 this.isProcessing = false; 53 } 54} 55 56 57class LogEvent extends Event { 58 constructor(message) { 59 super('log'); 60 this.message = message; 61 } 62} 63 64export class Bumble extends EventTarget { 65 constructor(pyodide) { 66 super(); 67 this.pyodide = pyodide; 68 } 69 70 async loadRuntime(bumblePackage) { 71 // Load pyodide if it isn't provided. 72 if (this.pyodide === undefined) { 73 this.log('Loading Pyodide'); 74 this.pyodide = await loadPyodide(); 75 } 76 77 // Load the Bumble module 78 bumblePackage ||= 'bumble'; 79 console.log('Installing micropip'); 80 this.log(`Installing ${bumblePackage}`) 81 await this.pyodide.loadPackage('micropip'); 82 await this.pyodide.runPythonAsync(` 83 import micropip 84 await micropip.install('${bumblePackage}') 85 package_list = micropip.list() 86 print(package_list) 87 `) 88 89 // Mount a filesystem so that we can persist data like the Key Store 90 let mountDir = '/bumble'; 91 this.pyodide.FS.mkdir(mountDir); 92 this.pyodide.FS.mount(this.pyodide.FS.filesystems.IDBFS, { root: '.' }, mountDir); 93 94 // Sync previously persisted filesystem data into memory 95 await new Promise(resolve => { 96 this.pyodide.FS.syncfs(true, () => { 97 console.log('FS synced in'); 98 resolve(); 99 }); 100 }) 101 102 // Setup the HCI source and sink 103 this.packetSource = new PacketSource(this.pyodide); 104 this.packetSink = new PacketSink(); 105 } 106 107 log(message) { 108 this.dispatchEvent(new LogEvent(message)); 109 } 110 111 async connectWebSocketTransport(hciWsUrl) { 112 return new Promise((resolve, reject) => { 113 let resolved = false; 114 115 let ws = new WebSocket(hciWsUrl); 116 ws.binaryType = 'arraybuffer'; 117 118 ws.onopen = () => { 119 this.log('WebSocket open'); 120 resolve(); 121 resolved = true; 122 } 123 124 ws.onclose = () => { 125 this.log('WebSocket close'); 126 if (!resolved) { 127 reject(`Failed to connect to ${hciWsUrl}`); 128 } 129 } 130 131 ws.onmessage = (event) => { 132 this.packetSource.data_received(event.data); 133 } 134 135 this.packetSink.writer = (packet) => { 136 if (ws.readyState === WebSocket.OPEN) { 137 ws.send(packet); 138 } 139 } 140 this.closeTransport = async () => { 141 if (ws.readyState === WebSocket.OPEN) { 142 ws.close(); 143 } 144 } 145 }) 146 } 147 148 async loadApp(appUrl) { 149 this.log('Loading app'); 150 const script = await (await fetch(appUrl)).text(); 151 await this.pyodide.runPythonAsync(script); 152 const pythonMain = this.pyodide.globals.get('main'); 153 const app = await pythonMain(this.packetSource, this.packetSink); 154 if (app.on) { 155 app.on('key_store_update', this.onKeystoreUpdate.bind(this)); 156 } 157 this.log('App is ready!'); 158 return app; 159 } 160 161 onKeystoreUpdate() { 162 // Sync the FS 163 this.pyodide.FS.syncfs(() => { 164 console.log('FS synced out'); 165 }); 166 } 167} 168 169export async function setupSimpleApp(appUrl, bumbleControls, log) { 170 // Load Bumble 171 log('Loading Bumble'); 172 const bumble = new Bumble(); 173 bumble.addEventListener('log', (event) => { 174 log(event.message); 175 }) 176 const params = (new URL(document.location)).searchParams; 177 await bumble.loadRuntime(params.get('package')); 178 179 log('Bumble is ready!') 180 const app = await bumble.loadApp(appUrl); 181 182 bumbleControls.connector = async (hciWsUrl) => { 183 try { 184 // Connect the WebSocket HCI transport 185 await bumble.connectWebSocketTransport(hciWsUrl); 186 187 // Start the app 188 await app.start(); 189 190 return true; 191 } catch (err) { 192 log(err); 193 return false; 194 } 195 } 196 bumbleControls.stopper = async () => { 197 // Stop the app 198 await app.stop(); 199 200 // Close the HCI transport 201 await bumble.closeTransport(); 202 } 203 bumbleControls.onBumbleLoaded(); 204 205 return app; 206} 207