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 console.log('Installing micropip'); 79 this.log(`Installing ${bumblePackage}`) 80 await this.pyodide.loadPackage('micropip'); 81 await this.pyodide.runPythonAsync(` 82 import micropip 83 await micropip.install('${bumblePackage}') 84 package_list = micropip.list() 85 print(package_list) 86 `) 87 88 // Mount a filesystem so that we can persist data like the Key Store 89 let mountDir = '/bumble'; 90 this.pyodide.FS.mkdir(mountDir); 91 this.pyodide.FS.mount(this.pyodide.FS.filesystems.IDBFS, { root: '.' }, mountDir); 92 93 // Sync previously persisted filesystem data into memory 94 await new Promise(resolve => { 95 this.pyodide.FS.syncfs(true, () => { 96 console.log('FS synced in'); 97 resolve(); 98 }); 99 }) 100 101 // Setup the HCI source and sink 102 this.packetSource = new PacketSource(this.pyodide); 103 this.packetSink = new PacketSink(); 104 } 105 106 log(message) { 107 this.dispatchEvent(new LogEvent(message)); 108 } 109 110 async connectWebSocketTransport(hciWsUrl) { 111 return new Promise((resolve, reject) => { 112 let resolved = false; 113 114 let ws = new WebSocket(hciWsUrl); 115 ws.binaryType = 'arraybuffer'; 116 117 ws.onopen = () => { 118 this.log('WebSocket open'); 119 resolve(); 120 resolved = true; 121 } 122 123 ws.onclose = () => { 124 this.log('WebSocket close'); 125 if (!resolved) { 126 reject(`Failed to connect to ${hciWsUrl}`); 127 } 128 } 129 130 ws.onmessage = (event) => { 131 this.packetSource.data_received(event.data); 132 } 133 134 this.packetSink.writer = (packet) => { 135 if (ws.readyState === WebSocket.OPEN) { 136 ws.send(packet); 137 } 138 } 139 this.closeTransport = async () => { 140 if (ws.readyState === WebSocket.OPEN) { 141 ws.close(); 142 } 143 } 144 }) 145 } 146 147 async loadApp(appUrl) { 148 this.log('Loading app'); 149 const script = await (await fetch(appUrl)).text(); 150 await this.pyodide.runPythonAsync(script); 151 const pythonMain = this.pyodide.globals.get('main'); 152 const app = await pythonMain(this.packetSource, this.packetSink); 153 if (app.on) { 154 app.on('key_store_update', this.onKeystoreUpdate.bind(this)); 155 } 156 this.log('App is ready!'); 157 return app; 158 } 159 160 onKeystoreUpdate() { 161 // Sync the FS 162 this.pyodide.FS.syncfs(() => { 163 console.log('FS synced out'); 164 }); 165 } 166} 167 168async function getBumblePackage() { 169 const params = (new URL(document.location)).searchParams; 170 // First check the packageFile override param 171 if (params.has('packageFile')) { 172 return await (await fetch('/packageFile')).text() 173 } 174 // Then check the package override param 175 if (params.has('package')) { 176 return params.get('package') 177 } 178 // If no override params, default to the main package 179 return 'bumble' 180} 181 182export async function setupSimpleApp(appUrl, bumbleControls, log) { 183 // Load Bumble 184 log('Loading Bumble'); 185 const bumble = new Bumble(); 186 bumble.addEventListener('log', (event) => { 187 log(event.message); 188 }) 189 await bumble.loadRuntime(await getBumblePackage()); 190 191 log('Bumble is ready!') 192 const app = await bumble.loadApp(appUrl); 193 194 bumbleControls.connector = async (hciWsUrl) => { 195 try { 196 // Connect the WebSocket HCI transport 197 await bumble.connectWebSocketTransport(hciWsUrl); 198 199 // Start the app 200 await app.start(); 201 202 return true; 203 } catch (err) { 204 log(err); 205 return false; 206 } 207 } 208 bumbleControls.stopper = async () => { 209 // Stop the app 210 await app.stop(); 211 212 // Close the HCI transport 213 await bumble.closeTransport(); 214 } 215 bumbleControls.onBumbleLoaded(); 216 217 return app; 218} 219