1<html data-bs-theme="dark">
2
3<head>
4    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
5        integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
6    <script src="https://unpkg.com/pcm-player"></script>
7</head>
8
9<body>
10    <nav class="navbar navbar-dark bg-primary">
11        <div class="container">
12            <span class="navbar-brand mb-0 h1">Bumble HFP Audio Gateway</span>
13        </div>
14    </nav>
15    <br>
16
17    <div class="container">
18
19        <label class="form-label">Send AT Response</label>
20        <div class="input-group mb-3">
21            <input type="text" class="form-control" placeholder="AT Response" aria-label="AT response" id="at_response">
22            <button class="btn btn-primary" type="button"
23                onclick="send_at_response(document.getElementById('at_response').value)">Send</button>
24        </div>
25
26        <div class="row">
27            <div class="col-3">
28                <label class="form-label">Speaker Volume</label>
29                <div class="input-group mb-3 col-auto">
30                    <input type="text" class="form-control" placeholder="0 - 15" aria-label="Speaker Volume"
31                        id="speaker_volume">
32                    <button class="btn btn-primary" type="button"
33                        onclick="send_at_response(`+VGS: ${document.getElementById('speaker_volume').value}`)">Set</button>
34                </div>
35            </div>
36            <div class="col-3">
37                <label class="form-label">Mic Volume</label>
38                <div class="input-group mb-3 col-auto">
39                    <input type="text" class="form-control" placeholder="0 - 15" aria-label="Mic Volume"
40                        id="mic_volume">
41                    <button class="btn btn-primary" type="button"
42                        onclick="send_at_response(`+VGM: ${document.getElementById('mic_volume').value}`)">Set</button>
43                </div>
44            </div>
45            <div class="col-3">
46                <label class="form-label">Browser Gain</label>
47                <input type="range" class="form-range" id="browser-gain" min="0" max="2" value="1" step="0.1" onchange="setGain()">
48            </div>
49        </div>
50
51        <div class="row">
52            <div class="col-auto">
53                <div class="input-group mb-3">
54                    <span class="input-group-text">Codec</span>
55
56                    <select class="form-select" id="codec">
57                        <option selected value="1">CVSD</option>
58                        <option value="2">MSBC</option>
59                    </select>
60                </div>
61            </div>
62
63            <div class="col-auto">
64                <button class="btn btn-primary" onclick="negotiate_codec()">Negotiate Codec</button>
65            </div>
66            <div class="col-auto">
67                <button class="btn btn-primary" onclick="connect_sco()">Connect SCO</button>
68            </div>
69            <div class="col-auto">
70                <button class="btn btn-primary" onclick="disconnect_sco()">Disconnect SCO</button>
71            </div>
72            <div class="col-auto">
73                <button class="btn btn-danger" onclick="connectAudio()">Connect Audio</button>
74            </div>
75        </div>
76
77        <hr>
78
79        <div class="row">
80            <h4>AG Indicators</h2>
81                <div class="col-3">
82                    <label class="form-label">call</label>
83                    <div class="input-group mb-3 col-auto">
84                        <select class="form-select" id="call">
85                            <option selected value="0">Inactive</option>
86                            <option value="1">Active</option>
87                        </select>
88                        <button class="btn btn-primary" type="button" onclick="update_ag_indicator('call')">Set</button>
89                    </div>
90                </div>
91                <div class="col-3">
92                    <label class="form-label">callsetup</label>
93                    <div class="input-group mb-3 col-auto">
94                        <select class="form-select" id="callsetup">
95                            <option selected value="0">Idle</option>
96                            <option value="1">Incoming</option>
97                            <option value="2">Outgoing</option>
98                            <option value="3">Remote Alerted</option>
99                        </select>
100                        <button class="btn btn-primary" type="button"
101                            onclick="update_ag_indicator('callsetup')">Set</button>
102                    </div>
103                </div>
104                <div class="col-3">
105                    <label class="form-label">callheld</label>
106                    <div class="input-group mb-3 col-auto">
107                        <select class="form-select" id="callsetup">
108                            <option selected value="0">0</option>
109                            <option value="1">1</option>
110                            <option value="2">2</option>
111                        </select>
112                        <button class="btn btn-primary" type="button"
113                            onclick="update_ag_indicator('callheld')">Set</button>
114                    </div>
115                </div>
116                <div class="col-3">
117                    <label class="form-label">signal</label>
118                    <div class="input-group mb-3 col-auto">
119                        <select class="form-select" id="signal">
120                            <option selected value="0">0</option>
121                            <option value="1">1</option>
122                            <option value="2">2</option>
123                            <option value="3">3</option>
124                            <option value="4">4</option>
125                            <option value="5">5</option>
126                        </select>
127                        <button class="btn btn-primary" type="button"
128                            onclick="update_ag_indicator('signal')">Set</button>
129                    </div>
130                </div>
131                <div class="col-3">
132                    <label class="form-label">roam</label>
133                    <div class="input-group mb-3 col-auto">
134                        <select class="form-select" id="roam">
135                            <option selected value="0">0</option>
136                            <option value="1">1</option>
137                        </select>
138                        <button class="btn btn-primary" type="button" onclick="update_ag_indicator('roam')">Set</button>
139                    </div>
140                </div>
141                <div class="col-3">
142                    <label class="form-label">battchg</label>
143                    <div class="input-group mb-3 col-auto">
144                        <select class="form-select" id="battchg">
145                            <option selected value="0">0</option>
146                            <option value="1">1</option>
147                            <option value="2">2</option>
148                            <option value="3">3</option>
149                            <option value="4">4</option>
150                            <option value="5">5</option>
151                        </select>
152                        <button class="btn btn-primary" type="button"
153                            onclick="update_ag_indicator('battchg')">Set</button>
154                    </div>
155                </div>
156                <div class="col-3">
157                    <label class="form-label">service</label>
158                    <div class="input-group mb-3 col-auto">
159                        <select class="form-select" id="service">
160                            <option selected value="0">0</option>
161                            <option value="1">1</option>
162                        </select>
163                        <button class="btn btn-primary" type="button"
164                            onclick="update_ag_indicator('service')">Set</button>
165                    </div>
166                </div>
167        </div>
168
169        <hr>
170
171        <button class="btn btn-primary" onclick="send_at_response('+BVRA: 1')">Start Voice Assistant</button>
172        <button class="btn btn-primary" onclick="send_at_response('+BVRA: 0')">Stop Voice Assistant</button>
173
174        <hr>
175
176
177        <h4>Calls</h4>
178        <div id="call-lists">
179            <template id="call-template">
180                <div class="row call-row">
181                    <div class="input-group mb-3">
182                        <label class="input-group-text">Index</label>
183                        <input class="form-control call-index" value="1">
184
185                        <label class="input-group-text">Number</label>
186                        <input class="form-control call-number">
187
188                        <label class="input-group-text">Direction</label>
189                        <select class="form-select call-direction">
190                            <option selected value="0">Originated</option>
191                            <option value="1">Terminated</option>
192                        </select>
193
194                        <label class="input-group-text">Status</label>
195                        <select class="form-select call-status">
196                            <option value="0">ACTIVE</option>
197                            <option value="1">HELD</option>
198                            <option value="2">DIALING</option>
199                            <option value="3">ALERTING</option>
200                            <option value="4">INCOMING</option>
201                            <option value="5">WAITING</option>
202                        </select>
203                        <button class="btn btn-primary call-remover">❌</button>
204                    </div>
205                </div>
206            </template>
207        </div>
208
209        <button class="btn btn-primary" onclick="add_call()">➕ Add Call</button>
210        <button class="btn btn-primary" onclick="update_calls()">�� Update Calls</button>
211
212        <hr>
213
214        <div id="socketStateContainer" class="bg-body-tertiary p-3 rounded-2">
215            <h3>Log</h3>
216            <code id="log" style="white-space: pre-line;"></code>
217        </div>
218    </div>
219
220
221    <script>
222        let atResponseInput = document.getElementById("at_response")
223        let gainInput = document.getElementById('browser-gain')
224        let log = document.getElementById("log")
225        let socket = new WebSocket('ws://localhost:8888');
226        let sampleRate = 0;
227        let player;
228
229        socket.binaryType = "arraybuffer";
230        socket.onopen = _ => {
231            log.textContent += 'SOCKET OPEN\n'
232        }
233        socket.onclose = _ => {
234            log.textContent += 'SOCKET CLOSED\n'
235        }
236        socket.onerror = (error) => {
237            log.textContent += 'SOCKET ERROR\n'
238            console.log(`ERROR: ${error}`)
239        }
240        socket.onmessage = function (message) {
241            if (typeof message.data === 'string' || message.data instanceof String) {
242                log.textContent += `<-- ${event.data}\n`
243                const jsonMessage = JSON.parse(event.data)
244
245                if (jsonMessage.type == 'speaker_volume') {
246                    document.getElementById('speaker_volume').value = jsonMessage.level;
247                } else if (jsonMessage.type == 'microphone_volume') {
248                    document.getElementById('microphone_volume').value = jsonMessage.level;
249                } else if (jsonMessage.type == 'sco_state_change') {
250                    sampleRate = jsonMessage.sample_rate;
251                    console.log(sampleRate);
252                    if (player != null) {
253                        player = new PCMPlayer({
254                            inputCodec: 'Int16',
255                            channels: 1,
256                            sampleRate: sampleRate,
257                            flushTime: 7.5,
258                        });
259                        player.volume(gainInput.value);
260                    }
261                }
262            } else {
263                // BINARY audio data.
264                if (player == null) return;
265                player.feed(message.data);
266            }
267        };
268
269        function send(message) {
270            if (socket && socket.readyState == WebSocket.OPEN) {
271                let jsonMessage = JSON.stringify(message)
272                log.textContent += `--> ${jsonMessage}\n`
273                socket.send(jsonMessage)
274            } else {
275                log.textContent += 'NOT CONNECTED\n'
276            }
277        }
278
279        function send_at_response(response) {
280            send({ type: 'at_response', response: response })
281        }
282
283        function update_ag_indicator(indicator) {
284            const value = document.getElementById(indicator).value
285            send({ type: 'ag_indicator', indicator: indicator, value: value })
286        }
287
288        function connect_sco() {
289            send({ type: 'connect_sco' })
290        }
291
292        function negotiate_codec() {
293            const codec = document.getElementById('codec').value
294            send({ type: 'negotiate_codec', codec: codec })
295        }
296
297        function disconnect_sco() {
298            send({ type: 'disconnect_sco' })
299        }
300
301        function add_call() {
302            let callLists = document.getElementById('call-lists');
303            let template = document.getElementById('call-template');
304
305            let newNode = document.importNode(template.content, true);
306            newNode.querySelector('.call-remover').onclick = function (event) {
307                event.target.closest('.call-row').remove();
308            }
309            callLists.appendChild(newNode);
310        }
311
312        function update_calls() {
313            let callLists = document.getElementById('call-lists');
314            send({
315                type: 'update_calls',
316                calls: Array.from(
317                    callLists.querySelectorAll('.call-row')).map(
318                        function (element) {
319                            return {
320                                index: element.querySelector('.call-index').value,
321                                number: element.querySelector('.call-number').value,
322                                direction: element.querySelector('.call-direction').value,
323                                status: element.querySelector('.call-status').value,
324                            }
325                        }
326                    ),
327            }
328            )
329        }
330
331        function connectAudio() {
332            player = new PCMPlayer({
333                inputCodec: 'Int16',
334                channels: 1,
335                sampleRate: sampleRate,
336                flushTime: 7.5,
337            });
338            player.volume(gainInput.value);
339        }
340
341        function setGain() {
342            if (player != null) {
343                player.volume(gainInput.value);
344            }
345        }
346    </script>
347    </div>
348</body>
349
350</html>