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>