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