• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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