mirror of
https://github.com/google/bumble.git
synced 2026-04-16 00:25:31 +00:00
Rework HFP example
This commit is contained in:
@@ -22,7 +22,8 @@ import dataclasses
|
|||||||
import enum
|
import enum
|
||||||
import traceback
|
import traceback
|
||||||
import pyee
|
import pyee
|
||||||
from typing import Dict, List, Union, Set, Any, Optional, TYPE_CHECKING
|
from typing import Dict, List, Union, Set, Any, Optional, Type, TYPE_CHECKING
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
from bumble import at
|
from bumble import at
|
||||||
from bumble import rfcomm
|
from bumble import rfcomm
|
||||||
@@ -417,17 +418,21 @@ class AtResponseType(enum.Enum):
|
|||||||
MULTIPLE = 2
|
MULTIPLE = 2
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
class AtResponse:
|
class AtResponse:
|
||||||
code: str
|
code: str
|
||||||
parameters: list
|
parameters: list
|
||||||
|
|
||||||
def __init__(self, response: bytearray):
|
@classmethod
|
||||||
code_and_parameters = response.split(b':')
|
def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
|
||||||
|
code_and_parameters = buffer.split(b':')
|
||||||
parameters = (
|
parameters = (
|
||||||
code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray()
|
code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray()
|
||||||
)
|
)
|
||||||
self.code = code_and_parameters[0].decode()
|
return cls(
|
||||||
self.parameters = at.parse_parameters(parameters)
|
code=code_and_parameters[0].decode(),
|
||||||
|
parameters=at.parse_parameters(parameters),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
@@ -530,7 +535,7 @@ class HfProtocol(pyee.EventEmitter):
|
|||||||
|
|
||||||
# Isolate the AT response code and parameters.
|
# Isolate the AT response code and parameters.
|
||||||
raw_response = self.read_buffer[header + 2 : trailer]
|
raw_response = self.read_buffer[header + 2 : trailer]
|
||||||
response = AtResponse(raw_response)
|
response = AtResponse.parse_from(raw_response)
|
||||||
logger.debug(f"<<< {raw_response.decode()}")
|
logger.debug(f"<<< {raw_response.decode()}")
|
||||||
|
|
||||||
# Consume the response bytes.
|
# Consume the response bytes.
|
||||||
@@ -1006,7 +1011,9 @@ class EscoParameters:
|
|||||||
transmit_coding_format: CodingFormat
|
transmit_coding_format: CodingFormat
|
||||||
receive_coding_format: CodingFormat
|
receive_coding_format: CodingFormat
|
||||||
packet_type: HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType
|
packet_type: HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType
|
||||||
retransmission_effort: HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort
|
retransmission_effort: (
|
||||||
|
HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort
|
||||||
|
)
|
||||||
max_latency: int
|
max_latency: int
|
||||||
|
|
||||||
# Common
|
# Common
|
||||||
@@ -1014,12 +1021,12 @@ class EscoParameters:
|
|||||||
output_coding_format: CodingFormat = CodingFormat(CodecID.LINEAR_PCM)
|
output_coding_format: CodingFormat = CodingFormat(CodecID.LINEAR_PCM)
|
||||||
input_coded_data_size: int = 16
|
input_coded_data_size: int = 16
|
||||||
output_coded_data_size: int = 16
|
output_coded_data_size: int = 16
|
||||||
input_pcm_data_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat = (
|
input_pcm_data_format: (
|
||||||
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
|
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat
|
||||||
)
|
) = HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
|
||||||
output_pcm_data_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat = (
|
output_pcm_data_format: (
|
||||||
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
|
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat
|
||||||
)
|
) = HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
|
||||||
input_pcm_sample_payload_msb_position: int = 0
|
input_pcm_sample_payload_msb_position: int = 0
|
||||||
output_pcm_sample_payload_msb_position: int = 0
|
output_pcm_sample_payload_msb_position: int = 0
|
||||||
input_data_path: HCI_Enhanced_Setup_Synchronous_Connection_Command.DataPath = (
|
input_data_path: HCI_Enhanced_Setup_Synchronous_Connection_Command.DataPath = (
|
||||||
|
|||||||
@@ -1,79 +1,132 @@
|
|||||||
<html>
|
<html data-bs-theme="dark">
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
<head>
|
||||||
display: block;
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
}
|
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-dark bg-primary">
|
||||||
|
<div class="container">
|
||||||
|
<span class="navbar-brand mb-0 h1">Bumble Handsfree</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<label class="form-label">Server Port</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" aria-label="Port Number" value="8989" id="port">
|
||||||
|
<button class="btn btn-primary" type="button" onclick="connect()">Connect</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="form-label">Dial Phone Number</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" placeholder="Phone Number" aria-label="Phone Number"
|
||||||
|
id="dial_number">
|
||||||
|
<button class="btn btn-primary" type="button"
|
||||||
|
onclick="send_at_command(`ATD${dialNumberInput.value}`)">Dial</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="form-label">Send AT Command</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" placeholder="AT Command" aria-label="AT command" id="at_command">
|
||||||
|
<button class="btn btn-primary" type="button"
|
||||||
|
onclick="send_at_command(document.getElementById('at_command').value)">Send</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label">Battery Level</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" placeholder="0 - 100" aria-label="Battery Level"
|
||||||
|
id="battery_level">
|
||||||
|
<button class="btn btn-primary" type="button"
|
||||||
|
onclick="send_at_command(`AT+BIEV=2,${document.getElementById('battery_level').value}`)">Set</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label">Speaker Volume</label>
|
||||||
|
<div class="input-group mb-3 col-auto">
|
||||||
|
<input type="text" class="form-control" placeholder="0 - 15" aria-label="Speaker Volume"
|
||||||
|
id="speaker_volume">
|
||||||
|
<button class="btn btn-primary" type="button"
|
||||||
|
onclick="send_at_command(`AT+VGS=${document.getElementById('speaker_volume').value}`)">Set</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label">Mic Volume</label>
|
||||||
|
<div class="input-group mb-3 col-auto">
|
||||||
|
<input type="text" class="form-control" placeholder="0 - 15" aria-label="Mic Volume"
|
||||||
|
id="mic_volume">
|
||||||
|
<button class="btn btn-primary" type="button"
|
||||||
|
onclick="send_at_command(`AT+VGM=${document.getElementById('mic_volume').value}`)">Set</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" onclick="send_at_command('ATA')">Answer</button>
|
||||||
|
<button class="btn btn-primary" onclick="send_at_command('AT+CHUP')">Hang Up</button>
|
||||||
|
<button class="btn btn-primary" onclick="send_at_command('AT+BLDN')">Redial</button>
|
||||||
|
<button class="btn btn-primary" onclick="send({ type: 'query_call'})">Get Call Status</button>
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" onclick="send_at_command('AT+BVRA=1')">Start Voice Assistant</button>
|
||||||
|
<button class="btn btn-primary" onclick="send_at_command('AT+BVRA=0')">Stop Voice Assistant</button>
|
||||||
|
|
||||||
input, label {
|
|
||||||
margin: .4rem 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Server Port <input id="port" type="text" value="8989"></input> <button onclick="connect()">Connect</button><br>
|
|
||||||
AT Command <input type="text" id="at_command" required size="10"> <button onclick="send_at_command()">Send</button><br>
|
|
||||||
Dial Phone Number <input type="text" id="dial_number" required size="10"> <button onclick="dial()">Dial</button><br>
|
|
||||||
<button onclick="answer()">Answer</button>
|
|
||||||
<button onclick="hangup()">Hang Up</button>
|
|
||||||
<button onclick="start_voice_assistant()">Start Voice Assistant</button>
|
|
||||||
<button onclick="stop_voice_assistant()">Stop Voice Assistant</button>
|
|
||||||
<hr>
|
<hr>
|
||||||
<div id="socketState"></div>
|
|
||||||
<script>
|
<div id="socketStateContainer" class="bg-body-tertiary p-3 rounded-2">
|
||||||
|
<h3>Log</h3>
|
||||||
|
<code id="log" style="white-space: pre-line;"></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
let portInput = document.getElementById("port")
|
let portInput = document.getElementById("port")
|
||||||
let atCommandInput = document.getElementById("at_command")
|
let atCommandInput = document.getElementById("at_command")
|
||||||
let dialNumberInput = document.getElementById("dial_number")
|
let log = document.getElementById("log")
|
||||||
let socketState = document.getElementById("socketState")
|
|
||||||
let socket
|
let socket
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
socket = new WebSocket(`ws://localhost:${portInput.value}`);
|
socket = new WebSocket(`ws://localhost:${portInput.value}`);
|
||||||
socket.onopen = _ => {
|
socket.onopen = _ => {
|
||||||
socketState.innerText = 'OPEN'
|
log.textContent += 'OPEN\n'
|
||||||
}
|
}
|
||||||
socket.onclose = _ => {
|
socket.onclose = _ => {
|
||||||
socketState.innerText = 'CLOSED'
|
log.textContent += 'CLOSED\n'
|
||||||
}
|
}
|
||||||
socket.onerror = (error) => {
|
socket.onerror = (error) => {
|
||||||
socketState.innerText = 'ERROR'
|
log.textContent += 'ERROR\n'
|
||||||
console.log(`ERROR: ${error}`)
|
console.log(`ERROR: ${error}`)
|
||||||
}
|
}
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
log.textContent += `<-- ${event.data}\n`
|
||||||
|
let volume_state = JSON.parse(event.data)
|
||||||
|
volumeSetting.value = volume_state.volume_setting
|
||||||
|
changeCounter.value = volume_state.change_counter
|
||||||
|
muted.checked = volume_state.muted ? true : false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(message) {
|
function send(message) {
|
||||||
if (socket && socket.readyState == WebSocket.OPEN) {
|
if (socket && socket.readyState == WebSocket.OPEN) {
|
||||||
socket.send(JSON.stringify(message))
|
let jsonMessage = JSON.stringify(message)
|
||||||
|
log.textContent += `--> ${jsonMessage}\n`
|
||||||
|
socket.send(jsonMessage)
|
||||||
|
} else {
|
||||||
|
log.textContent += 'NOT CONNECTED\n'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function send_at_command() {
|
function send_at_command(command) {
|
||||||
send({ type:'at_command', command: atCommandInput.value })
|
send({ type: 'at_command', 'command': command })
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
function answer() {
|
</html>
|
||||||
send({ type:'at_command', command: 'ATA' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function hangup() {
|
|
||||||
send({ type:'at_command', command: 'AT+CHUP' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function dial() {
|
|
||||||
send({ type:'at_command', command: `ATD${dialNumberInput.value}` })
|
|
||||||
}
|
|
||||||
|
|
||||||
function start_voice_assistant() {
|
|
||||||
send(({ type:'at_command', command: 'AT+BVRA=1' }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop_voice_assistant() {
|
|
||||||
send(({ type:'at_command', command: 'AT+BVRA=0' }))
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
@@ -31,39 +32,16 @@ from bumble.transport import open_transport_or_link
|
|||||||
from bumble import hfp
|
from bumble import hfp
|
||||||
from bumble.hfp import HfProtocol
|
from bumble.hfp import HfProtocol
|
||||||
|
|
||||||
|
ws: Optional[websockets.WebSocketServerProtocol] = None
|
||||||
# -----------------------------------------------------------------------------
|
hf_protocol: Optional[HfProtocol] = None
|
||||||
class UiServer:
|
|
||||||
protocol: Optional[HfProtocol] = None
|
|
||||||
|
|
||||||
async def start(self):
|
|
||||||
"""Start a Websocket server to receive events from a web page."""
|
|
||||||
|
|
||||||
async def serve(websocket, _path):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
message = await websocket.recv()
|
|
||||||
print('Received: ', str(message))
|
|
||||||
|
|
||||||
parsed = json.loads(message)
|
|
||||||
message_type = parsed['type']
|
|
||||||
if message_type == 'at_command':
|
|
||||||
if self.protocol is not None:
|
|
||||||
await self.protocol.execute_command(parsed['command'])
|
|
||||||
|
|
||||||
except websockets.exceptions.ConnectionClosedOK:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
|
||||||
await websockets.serve(serve, 'localhost', 8989)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def on_dlc(dlc: rfcomm.DLC, configuration: hfp.Configuration):
|
def on_dlc(dlc: rfcomm.DLC, configuration: hfp.Configuration):
|
||||||
print('*** DLC connected', dlc)
|
print('*** DLC connected', dlc)
|
||||||
protocol = HfProtocol(dlc, configuration)
|
global hf_protocol
|
||||||
UiServer.protocol = protocol
|
hf_protocol = HfProtocol(dlc, configuration)
|
||||||
asyncio.create_task(protocol.run())
|
asyncio.create_task(hf_protocol.run())
|
||||||
|
|
||||||
def on_sco_request(connection: Connection, link_type: int, protocol: HfProtocol):
|
def on_sco_request(connection: Connection, link_type: int, protocol: HfProtocol):
|
||||||
if connection == protocol.dlc.multiplexer.l2cap_channel.connection:
|
if connection == protocol.dlc.multiplexer.l2cap_channel.connection:
|
||||||
@@ -88,7 +66,7 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.Configuration):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
handler = functools.partial(on_sco_request, protocol=protocol)
|
handler = functools.partial(on_sco_request, protocol=hf_protocol)
|
||||||
dlc.multiplexer.l2cap_channel.connection.device.on('sco_request', handler)
|
dlc.multiplexer.l2cap_channel.connection.device.on('sco_request', handler)
|
||||||
dlc.multiplexer.l2cap_channel.once(
|
dlc.multiplexer.l2cap_channel.once(
|
||||||
'close',
|
'close',
|
||||||
@@ -97,6 +75,13 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.Configuration):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def on_ag_indicator(indicator):
|
||||||
|
global ws
|
||||||
|
if ws:
|
||||||
|
asyncio.create_task(ws.send(str(indicator)))
|
||||||
|
|
||||||
|
hf_protocol.on('ag_indicator', on_ag_indicator)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def main():
|
async def main():
|
||||||
@@ -154,8 +139,30 @@ async def main():
|
|||||||
await device.set_connectable(True)
|
await device.set_connectable(True)
|
||||||
|
|
||||||
# Start the UI websocket server to offer a few buttons and input boxes
|
# Start the UI websocket server to offer a few buttons and input boxes
|
||||||
ui_server = UiServer()
|
async def serve(websocket: websockets.WebSocketServerProtocol, _path):
|
||||||
await ui_server.start()
|
global ws
|
||||||
|
ws = websocket
|
||||||
|
async for message in websocket:
|
||||||
|
with contextlib.suppress(websockets.exceptions.ConnectionClosedOK):
|
||||||
|
print('Received: ', str(message))
|
||||||
|
|
||||||
|
parsed = json.loads(message)
|
||||||
|
message_type = parsed['type']
|
||||||
|
if message_type == 'at_command':
|
||||||
|
if hf_protocol is not None:
|
||||||
|
response = str(
|
||||||
|
await hf_protocol.execute_command(
|
||||||
|
parsed['command'],
|
||||||
|
response_type=hfp.AtResponseType.MULTIPLE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await websocket.send(response)
|
||||||
|
elif message_type == 'query_call':
|
||||||
|
if hf_protocol:
|
||||||
|
response = str(await hf_protocol.query_current_calls())
|
||||||
|
await websocket.send(response)
|
||||||
|
|
||||||
|
await websockets.serve(serve, 'localhost', 8989)
|
||||||
|
|
||||||
await hci_source.wait_for_termination()
|
await hci_source.wait_for_termination()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user