blob: 33b62f694649072a3f7b47ac295cd8c755e8e422 [file] [log] [blame] [edit]
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;
}