forked from auracaster/bumble_mirror
Compare commits
166 Commits
v0.0.158
...
gbg/androi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc6b466a42 | ||
|
|
5e5c9c2580 | ||
|
|
246b11925c | ||
|
|
dfa9131192 | ||
|
|
88c801b4c2 | ||
|
|
a1b55b94e0 | ||
|
|
80db9e2e2f | ||
|
|
ce74690420 | ||
|
|
50de4dfb5d | ||
|
|
9bcdf860f4 | ||
|
|
511ab4b630 | ||
|
|
6f2b623e3c | ||
|
|
fa12165cd3 | ||
|
|
c0c6f3329d | ||
|
|
406a932467 | ||
|
|
cc96d4245f | ||
|
|
c6cdca8923 | ||
|
|
7e331c2944 | ||
|
|
10347765cb | ||
|
|
c12dee4e76 | ||
|
|
772c188674 | ||
|
|
7c1a3bb8f9 | ||
|
|
8c3c0b1e13 | ||
|
|
1ad84ad51c | ||
|
|
64937c3f77 | ||
|
|
50fd2218fa | ||
|
|
4c29a16271 | ||
|
|
762d3e92de | ||
|
|
2f97531d78 | ||
|
|
f6c7cae661 | ||
|
|
f1777a5bd2 | ||
|
|
78a06ae8cf | ||
|
|
d290df4aa9 | ||
|
|
e559744f32 | ||
|
|
67418e649a | ||
|
|
5adf9fab53 | ||
|
|
2491b686fa | ||
|
|
efd02b2f3e | ||
|
|
3b14078646 | ||
|
|
eb9d5632bc | ||
|
|
45f60edbb6 | ||
|
|
393ea6a7bb | ||
|
|
6ec6f1efe5 | ||
|
|
5d9598ea51 | ||
|
|
0d36d99a73 | ||
|
|
d8a9f5a724 | ||
|
|
2c66e1a042 | ||
|
|
d5eccdb00f | ||
|
|
32626573a6 | ||
|
|
caa82b8f7e | ||
|
|
5af347b499 | ||
|
|
4ed5bb5a9e | ||
|
|
2478d45673 | ||
|
|
1bc7d94111 | ||
|
|
6432414cd5 | ||
|
|
179064ba15 | ||
|
|
783b2d70a5 | ||
|
|
80824f3fc1 | ||
|
|
f39f5f531c | ||
|
|
56139c622f | ||
|
|
da02f6a39b | ||
|
|
548d5597c0 | ||
|
|
7fd65d2412 | ||
|
|
05a54a4af9 | ||
|
|
1e00c8f456 | ||
|
|
90d165aa01 | ||
|
|
01603ca9e4 | ||
|
|
a1b6eb61f2 | ||
|
|
25f300d3ec | ||
|
|
41fe63df06 | ||
|
|
b312170d5f | ||
|
|
cf7f2e8f44 | ||
|
|
d292083ed1 | ||
|
|
9b11142b45 | ||
|
|
acdbc4d7b9 | ||
|
|
838d10a09d | ||
|
|
3852aa056b | ||
|
|
ae77e4528f | ||
|
|
9303f4fc5b | ||
|
|
8be9f4cb0e | ||
|
|
1ea12b1bf7 | ||
|
|
65e6d68355 | ||
|
|
9732eb8836 | ||
|
|
5ae668bc70 | ||
|
|
fd4d1bcca3 | ||
|
|
0a251c9f8e | ||
|
|
351d77be59 | ||
|
|
0e2fc80509 | ||
|
|
8f3fdecb93 | ||
|
|
249a205d8e | ||
|
|
7485801222 | ||
|
|
4678e59737 | ||
|
|
952d351c00 | ||
|
|
901eb55b0e | ||
|
|
727586e40e | ||
|
|
3aa678a58e | ||
|
|
fc7c1a8113 | ||
|
|
f62a0bbe75 | ||
|
|
7341172739 | ||
|
|
91b9fbe450 | ||
|
|
e6b566b848 | ||
|
|
2527a711dc | ||
|
|
5fba6b1cae | ||
|
|
43e632f83c | ||
|
|
623298b0e9 | ||
|
|
85a61dc39d | ||
|
|
6e8c44b5e6 | ||
|
|
ec4dcc174e | ||
|
|
b247aca3b4 | ||
|
|
6226bfd196 | ||
|
|
71e11b7cf8 | ||
|
|
800c62fdb6 | ||
|
|
640b9cd53a | ||
|
|
f4add16aea | ||
|
|
2bfec3c4ed | ||
|
|
9963b51c04 | ||
|
|
2af3494d8c | ||
|
|
fe28473ba8 | ||
|
|
53d66bc74a | ||
|
|
e2c1ad5342 | ||
|
|
6399c5fb04 | ||
|
|
784cf4f26a | ||
|
|
0301b1a999 | ||
|
|
3ab2cd5e71 | ||
|
|
6ea669531a | ||
|
|
cbbada4748 | ||
|
|
152b8d1233 | ||
|
|
bdad225033 | ||
|
|
8eeb58e467 | ||
|
|
91971433d2 | ||
|
|
a0a4bd457f | ||
|
|
4ffc050eed | ||
|
|
60678419a0 | ||
|
|
648dcc9305 | ||
|
|
190529184e | ||
|
|
46eb81466d | ||
|
|
9c70c487b9 | ||
|
|
43234d7c3e | ||
|
|
dbf878dc3f | ||
|
|
f6c0bd88d7 | ||
|
|
8440b7fbf1 | ||
|
|
808ab54135 | ||
|
|
52b29ad680 | ||
|
|
d41bf9c587 | ||
|
|
b758825164 | ||
|
|
779dfe5473 | ||
|
|
afb21220e2 | ||
|
|
f9a4c7518e | ||
|
|
bad2fdf69f | ||
|
|
a84df469cd | ||
|
|
03e33e39bd | ||
|
|
753fb69272 | ||
|
|
81a5f3a395 | ||
|
|
696a8d82fd | ||
|
|
5f294b1fea | ||
|
|
2d8f5e80fb | ||
|
|
7a042db78e | ||
|
|
41ce311836 | ||
|
|
03538d0f8a | ||
|
|
86bc222dc0 | ||
|
|
e8d285fdab | ||
|
|
852c933c92 | ||
|
|
7867a99a54 | ||
|
|
6cd14bb503 | ||
|
|
532b99ffea | ||
|
|
d80f40ff5d |
4
.github/workflows/code-check.yml
vendored
4
.github/workflows/code-check.yml
vendored
@@ -14,6 +14,10 @@ jobs:
|
||||
check:
|
||||
name: Check Code
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
|
||||
43
.github/workflows/python-build-test.yml
vendored
43
.github/workflows/python-build-test.yml
vendored
@@ -12,11 +12,11 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
@@ -41,3 +41,40 @@ jobs:
|
||||
run: |
|
||||
inv build
|
||||
inv build.mkdocs
|
||||
|
||||
build-rust:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
|
||||
rust-version: [ "1.70.0", "stable" ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install ".[build,test,development,documentation]"
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: clippy,rustfmt
|
||||
toolchain: ${{ matrix.rust-version }}
|
||||
- name: Check License Headers
|
||||
run: cd rust && cargo run --features dev-tools --bin file-header check-all
|
||||
- name: Rust Build
|
||||
run: cd rust && cargo build --all-targets && cargo build --all-features --all-targets
|
||||
# Lints after build so what clippy needs is already built
|
||||
- name: Rust Lints
|
||||
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings
|
||||
- name: Rust Tests
|
||||
run: cd rust && cargo test
|
||||
# At some point, hook up publishing the binary. For now, just make sure it builds.
|
||||
# Once we're ready to publish binaries, this should be built with `--release`.
|
||||
- name: Build Bumble CLI
|
||||
run: cd rust && cargo build --features bumble-tools --bin bumble
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ __pycache__
|
||||
# generated by setuptools_scm
|
||||
bumble/_version.py
|
||||
.vscode/launch.json
|
||||
/.idea
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -39,10 +39,12 @@
|
||||
"libusb",
|
||||
"MITM",
|
||||
"NDIS",
|
||||
"netsim",
|
||||
"NONBLOCK",
|
||||
"NONCONN",
|
||||
"OXIMETER",
|
||||
"popleft",
|
||||
"protobuf",
|
||||
"psms",
|
||||
"pyee",
|
||||
"pyusb",
|
||||
|
||||
@@ -1172,7 +1172,7 @@ class ScanResult:
|
||||
name = ''
|
||||
|
||||
# Remove any '/P' qualifier suffix from the address string
|
||||
address_str = str(self.address).replace('/P', '')
|
||||
address_str = self.address.to_string(with_type_qualifier=False)
|
||||
|
||||
# RSSI bar
|
||||
bar_string = rssi_bar(self.rssi)
|
||||
|
||||
@@ -63,7 +63,8 @@ async def get_classic_info(host):
|
||||
if command_succeeded(response):
|
||||
print()
|
||||
print(
|
||||
color('Classic Address:', 'yellow'), response.return_parameters.bd_addr
|
||||
color('Classic Address:', 'yellow'),
|
||||
response.return_parameters.bd_addr.to_string(False),
|
||||
)
|
||||
|
||||
if host.supports_command(HCI_READ_LOCAL_NAME_COMMAND):
|
||||
|
||||
@@ -105,7 +105,7 @@ class ServerBridge:
|
||||
asyncio.create_task(self.pipe.l2cap_channel.disconnect())
|
||||
|
||||
def data_received(self, data):
|
||||
print(f'<<< Received on TCP: {len(data)}')
|
||||
print(color(f'<<< [TCP DATA]: {len(data)} bytes', 'blue'))
|
||||
self.pipe.l2cap_channel.write(data)
|
||||
|
||||
try:
|
||||
@@ -123,6 +123,7 @@ class ServerBridge:
|
||||
await self.l2cap_channel.disconnect()
|
||||
|
||||
def on_l2cap_close(self):
|
||||
print(color('*** L2CAP channel closed', 'red'))
|
||||
self.l2cap_channel = None
|
||||
if self.tcp_transport is not None:
|
||||
self.tcp_transport.close()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
import click
|
||||
import logging
|
||||
import json
|
||||
|
||||
from bumble.pandora import PandoraDevice, serve
|
||||
from bumble.pandora import PandoraDevice, Config, serve
|
||||
from typing import Dict, Any
|
||||
|
||||
BUMBLE_SERVER_GRPC_PORT = 7999
|
||||
ROOTCANAL_PORT_CUTTLEFISH = 7300
|
||||
@@ -18,12 +20,31 @@ ROOTCANAL_PORT_CUTTLEFISH = 7300
|
||||
help='HCI transport',
|
||||
default=f'tcp-client:127.0.0.1:<rootcanal-port>',
|
||||
)
|
||||
def main(grpc_port: int, rootcanal_port: int, transport: str) -> None:
|
||||
@click.option(
|
||||
'--config',
|
||||
help='Bumble json configuration file',
|
||||
)
|
||||
def main(grpc_port: int, rootcanal_port: int, transport: str, config: str) -> None:
|
||||
if '<rootcanal-port>' in transport:
|
||||
transport = transport.replace('<rootcanal-port>', str(rootcanal_port))
|
||||
device = PandoraDevice({'transport': transport})
|
||||
|
||||
bumble_config = retrieve_config(config)
|
||||
bumble_config.setdefault('transport', transport)
|
||||
device = PandoraDevice(bumble_config)
|
||||
|
||||
server_config = Config()
|
||||
server_config.load_from_dict(bumble_config.get('server', {}))
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
asyncio.run(serve(device, port=grpc_port))
|
||||
asyncio.run(serve(device, config=server_config, port=grpc_port))
|
||||
|
||||
|
||||
def retrieve_config(config: str) -> Dict[str, Any]:
|
||||
if not config:
|
||||
return {}
|
||||
|
||||
with open(config, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
15
apps/show.py
15
apps/show.py
@@ -102,9 +102,21 @@ class SnoopPacketReader:
|
||||
default='h4',
|
||||
help='Format of the input file',
|
||||
)
|
||||
@click.option(
|
||||
'--vendors',
|
||||
type=click.Choice(['android', 'zephyr']),
|
||||
multiple=True,
|
||||
help='Support vendor-specific commands (list one or more)',
|
||||
)
|
||||
@click.argument('filename')
|
||||
# pylint: disable=redefined-builtin
|
||||
def main(format, filename):
|
||||
def main(format, vendors, filename):
|
||||
for vendor in vendors:
|
||||
if vendor == 'android':
|
||||
import bumble.vendor.android.hci
|
||||
elif vendor == 'zephyr':
|
||||
import bumble.vendor.zephyr.hci
|
||||
|
||||
input = open(filename, 'rb')
|
||||
if format == 'h4':
|
||||
packet_reader = PacketReader(input)
|
||||
@@ -124,7 +136,6 @@ def main(format, filename):
|
||||
if packet is None:
|
||||
break
|
||||
tracer.trace(hci.HCI_Packet.from_bytes(packet), direction)
|
||||
|
||||
except Exception as error:
|
||||
print(color(f'!!! {error}', 'red'))
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ body, h1, h2, h3, h4, h5, h6 {
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
margin: 6px;
|
||||
margin-left: 0px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
@@ -65,7 +65,7 @@ th, td {
|
||||
}
|
||||
|
||||
.properties td:nth-child(even) {
|
||||
background-color: #D6EEEE;
|
||||
background-color: #d6eeee;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Bumble Speaker</title>
|
||||
<script type="text/javascript" src="speaker.js"></script>
|
||||
<script src="speaker.js"></script>
|
||||
<link rel="stylesheet" href="speaker.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -195,7 +195,7 @@ class WebSocketOutput(QueuedOutput):
|
||||
except HCI_StatusError:
|
||||
pass
|
||||
peer_name = '' if connection.peer_name is None else connection.peer_name
|
||||
peer_address = str(connection.peer_address).replace('/P', '')
|
||||
peer_address = connection.peer_address.to_string(False)
|
||||
await self.send_message(
|
||||
'connection',
|
||||
peer_address=peer_address,
|
||||
@@ -228,10 +228,11 @@ class FfplayOutput(QueuedOutput):
|
||||
subprocess: Optional[asyncio.subprocess.Process]
|
||||
ffplay_task: Optional[asyncio.Task]
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(AacAudioExtractor())
|
||||
def __init__(self, codec: str) -> None:
|
||||
super().__init__(AudioExtractor.create(codec))
|
||||
self.subprocess = None
|
||||
self.ffplay_task = None
|
||||
self.codec = codec
|
||||
|
||||
async def start(self):
|
||||
if self.started:
|
||||
@@ -240,7 +241,7 @@ class FfplayOutput(QueuedOutput):
|
||||
await super().start()
|
||||
|
||||
self.subprocess = await asyncio.create_subprocess_shell(
|
||||
'ffplay -acodec aac pipe:0',
|
||||
f'ffplay -f {self.codec} pipe:0',
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
@@ -375,7 +376,7 @@ class UiServer:
|
||||
if connection := self.speaker().connection:
|
||||
await self.send_message(
|
||||
'connection',
|
||||
peer_address=str(connection.peer_address).replace('/P', ''),
|
||||
peer_address=connection.peer_address.to_string(False),
|
||||
peer_name=connection.peer_name,
|
||||
)
|
||||
|
||||
@@ -419,7 +420,7 @@ class Speaker:
|
||||
self.outputs = []
|
||||
for output in outputs:
|
||||
if output == '@ffplay':
|
||||
self.outputs.append(FfplayOutput())
|
||||
self.outputs.append(FfplayOutput(codec))
|
||||
continue
|
||||
|
||||
# Default to FileOutput
|
||||
@@ -708,17 +709,6 @@ def speaker(
|
||||
):
|
||||
"""Run the speaker."""
|
||||
|
||||
# ffplay only works with AAC for now
|
||||
if codec != 'aac' and '@ffplay' in output:
|
||||
print(
|
||||
color(
|
||||
f'{codec} not supported with @ffplay output, '
|
||||
'@ffplay output will be skipped',
|
||||
'yellow',
|
||||
)
|
||||
)
|
||||
output = list(filter(lambda x: x != '@ffplay', output))
|
||||
|
||||
if '@ffplay' in output:
|
||||
# Check if ffplay is installed
|
||||
try:
|
||||
|
||||
85
bumble/at.py
Normal file
85
bumble/at.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# Copyright 2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
|
||||
def tokenize_parameters(buffer: bytes) -> List[bytes]:
|
||||
"""Split input parameters into tokens.
|
||||
Removes space characters outside of double quote blocks:
|
||||
T-rec-V-25 - 5.2.1 Command line general format: "Space characters (IA5 2/0)
|
||||
are ignored [..], unless they are embedded in numeric or string constants"
|
||||
Raises ValueError in case of invalid input string."""
|
||||
|
||||
tokens = []
|
||||
in_quotes = False
|
||||
token = bytearray()
|
||||
for b in buffer:
|
||||
char = bytearray([b])
|
||||
|
||||
if in_quotes:
|
||||
token.extend(char)
|
||||
if char == b'\"':
|
||||
in_quotes = False
|
||||
tokens.append(token[1:-1])
|
||||
token = bytearray()
|
||||
else:
|
||||
if char == b' ':
|
||||
pass
|
||||
elif char == b',' or char == b')':
|
||||
tokens.append(token)
|
||||
tokens.append(char)
|
||||
token = bytearray()
|
||||
elif char == b'(':
|
||||
if len(token) > 0:
|
||||
raise ValueError("open_paren following regular character")
|
||||
tokens.append(char)
|
||||
elif char == b'"':
|
||||
if len(token) > 0:
|
||||
raise ValueError("quote following regular character")
|
||||
in_quotes = True
|
||||
token.extend(char)
|
||||
else:
|
||||
token.extend(char)
|
||||
|
||||
tokens.append(token)
|
||||
return [bytes(token) for token in tokens if len(token) > 0]
|
||||
|
||||
|
||||
def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
|
||||
"""Parse the parameters using the comma and parenthesis separators.
|
||||
Raises ValueError in case of invalid input string."""
|
||||
|
||||
tokens = tokenize_parameters(buffer)
|
||||
accumulator: List[list] = [[]]
|
||||
current: Union[bytes, list] = bytes()
|
||||
|
||||
for token in tokens:
|
||||
if token == b',':
|
||||
accumulator[-1].append(current)
|
||||
current = bytes()
|
||||
elif token == b'(':
|
||||
accumulator.append([])
|
||||
elif token == b')':
|
||||
if len(accumulator) < 2:
|
||||
raise ValueError("close_paren without matching open_paren")
|
||||
accumulator[-1].append(current)
|
||||
current = accumulator.pop()
|
||||
else:
|
||||
current = token
|
||||
|
||||
accumulator[-1].append(current)
|
||||
if len(accumulator) > 1:
|
||||
raise ValueError("missing close_paren")
|
||||
return accumulator[0]
|
||||
124
bumble/att.py
124
bumble/att.py
@@ -23,13 +23,14 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import enum
|
||||
import functools
|
||||
import struct
|
||||
from pyee import EventEmitter
|
||||
from typing import Dict, Type, TYPE_CHECKING
|
||||
from typing import Dict, Type, List, Protocol, Union, Optional, Any, TYPE_CHECKING
|
||||
|
||||
from bumble.core import UUID, name_or_number, get_dict_key_by_value, ProtocolError
|
||||
from bumble.hci import HCI_Object, key_with_value, HCI_Constant
|
||||
from bumble.core import UUID, name_or_number, ProtocolError
|
||||
from bumble.hci import HCI_Object, key_with_value
|
||||
from bumble.colors import color
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -182,6 +183,7 @@ UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
||||
# pylint: enable=line-too-long
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -209,7 +211,7 @@ class ATT_PDU:
|
||||
|
||||
pdu_classes: Dict[int, Type[ATT_PDU]] = {}
|
||||
op_code = 0
|
||||
name = None
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(pdu):
|
||||
@@ -719,48 +721,68 @@ class ATT_Handle_Value_Confirmation(ATT_PDU):
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ConnectionValue(Protocol):
|
||||
def read(self, connection) -> bytes:
|
||||
...
|
||||
|
||||
def write(self, connection, value: bytes) -> None:
|
||||
...
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Attribute(EventEmitter):
|
||||
# Permission flags
|
||||
READABLE = 0x01
|
||||
WRITEABLE = 0x02
|
||||
READ_REQUIRES_ENCRYPTION = 0x04
|
||||
WRITE_REQUIRES_ENCRYPTION = 0x08
|
||||
READ_REQUIRES_AUTHENTICATION = 0x10
|
||||
WRITE_REQUIRES_AUTHENTICATION = 0x20
|
||||
READ_REQUIRES_AUTHORIZATION = 0x40
|
||||
WRITE_REQUIRES_AUTHORIZATION = 0x80
|
||||
class Permissions(enum.IntFlag):
|
||||
READABLE = 0x01
|
||||
WRITEABLE = 0x02
|
||||
READ_REQUIRES_ENCRYPTION = 0x04
|
||||
WRITE_REQUIRES_ENCRYPTION = 0x08
|
||||
READ_REQUIRES_AUTHENTICATION = 0x10
|
||||
WRITE_REQUIRES_AUTHENTICATION = 0x20
|
||||
READ_REQUIRES_AUTHORIZATION = 0x40
|
||||
WRITE_REQUIRES_AUTHORIZATION = 0x80
|
||||
|
||||
PERMISSION_NAMES = {
|
||||
READABLE: 'READABLE',
|
||||
WRITEABLE: 'WRITEABLE',
|
||||
READ_REQUIRES_ENCRYPTION: 'READ_REQUIRES_ENCRYPTION',
|
||||
WRITE_REQUIRES_ENCRYPTION: 'WRITE_REQUIRES_ENCRYPTION',
|
||||
READ_REQUIRES_AUTHENTICATION: 'READ_REQUIRES_AUTHENTICATION',
|
||||
WRITE_REQUIRES_AUTHENTICATION: 'WRITE_REQUIRES_AUTHENTICATION',
|
||||
READ_REQUIRES_AUTHORIZATION: 'READ_REQUIRES_AUTHORIZATION',
|
||||
WRITE_REQUIRES_AUTHORIZATION: 'WRITE_REQUIRES_AUTHORIZATION',
|
||||
}
|
||||
@classmethod
|
||||
def from_string(cls, permissions_str: str) -> Attribute.Permissions:
|
||||
try:
|
||||
return functools.reduce(
|
||||
lambda x, y: x | Attribute.Permissions[y],
|
||||
permissions_str.replace('|', ',').split(","),
|
||||
Attribute.Permissions(0),
|
||||
)
|
||||
except TypeError as exc:
|
||||
# The check for `p.name is not None` here is needed because for InFlag
|
||||
# enums, the .name property can be None, when the enum value is 0,
|
||||
# so the type hint for .name is Optional[str].
|
||||
enum_list: List[str] = [p.name for p in cls if p.name is not None]
|
||||
enum_list_str = ",".join(enum_list)
|
||||
raise TypeError(
|
||||
f"Attribute::permissions error:\nExpected a string containing any of the keys, separated by commas: {enum_list_str }\nGot: {permissions_str}"
|
||||
) from exc
|
||||
|
||||
@staticmethod
|
||||
def string_to_permissions(permissions_str: str):
|
||||
try:
|
||||
return functools.reduce(
|
||||
lambda x, y: x | get_dict_key_by_value(Attribute.PERMISSION_NAMES, y),
|
||||
permissions_str.split(","),
|
||||
0,
|
||||
)
|
||||
except TypeError as exc:
|
||||
raise TypeError(
|
||||
f"Attribute::permissions error:\nExpected a string containing any of the keys, separated by commas: {','.join(Attribute.PERMISSION_NAMES.values())}\nGot: {permissions_str}"
|
||||
) from exc
|
||||
# Permission flags(legacy-use only)
|
||||
READABLE = Permissions.READABLE
|
||||
WRITEABLE = Permissions.WRITEABLE
|
||||
READ_REQUIRES_ENCRYPTION = Permissions.READ_REQUIRES_ENCRYPTION
|
||||
WRITE_REQUIRES_ENCRYPTION = Permissions.WRITE_REQUIRES_ENCRYPTION
|
||||
READ_REQUIRES_AUTHENTICATION = Permissions.READ_REQUIRES_AUTHENTICATION
|
||||
WRITE_REQUIRES_AUTHENTICATION = Permissions.WRITE_REQUIRES_AUTHENTICATION
|
||||
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
||||
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
||||
|
||||
def __init__(self, attribute_type, permissions, value=b''):
|
||||
value: Union[str, bytes, ConnectionValue]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attribute_type: Union[str, bytes, UUID],
|
||||
permissions: Union[str, Attribute.Permissions],
|
||||
value: Union[str, bytes, ConnectionValue] = b'',
|
||||
) -> None:
|
||||
EventEmitter.__init__(self)
|
||||
self.handle = 0
|
||||
self.end_group_handle = 0
|
||||
if isinstance(permissions, str):
|
||||
self.permissions = self.string_to_permissions(permissions)
|
||||
self.permissions = Attribute.Permissions.from_string(permissions)
|
||||
else:
|
||||
self.permissions = permissions
|
||||
|
||||
@@ -778,22 +800,26 @@ class Attribute(EventEmitter):
|
||||
else:
|
||||
self.value = value
|
||||
|
||||
def encode_value(self, value):
|
||||
def encode_value(self, value: Any) -> bytes:
|
||||
return value
|
||||
|
||||
def decode_value(self, value_bytes):
|
||||
def decode_value(self, value_bytes: bytes) -> Any:
|
||||
return value_bytes
|
||||
|
||||
def read_value(self, connection: Connection):
|
||||
def read_value(self, connection: Optional[Connection]) -> bytes:
|
||||
if (
|
||||
self.permissions & self.READ_REQUIRES_ENCRYPTION
|
||||
) and not connection.encryption:
|
||||
(self.permissions & self.READ_REQUIRES_ENCRYPTION)
|
||||
and connection is not None
|
||||
and not connection.encryption
|
||||
):
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_ENCRYPTION_ERROR, att_handle=self.handle
|
||||
)
|
||||
if (
|
||||
self.permissions & self.READ_REQUIRES_AUTHENTICATION
|
||||
) and not connection.authenticated:
|
||||
(self.permissions & self.READ_REQUIRES_AUTHENTICATION)
|
||||
and connection is not None
|
||||
and not connection.authenticated
|
||||
):
|
||||
raise ATT_Error(
|
||||
error_code=ATT_INSUFFICIENT_AUTHENTICATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
@@ -803,9 +829,9 @@ class Attribute(EventEmitter):
|
||||
error_code=ATT_INSUFFICIENT_AUTHORIZATION_ERROR, att_handle=self.handle
|
||||
)
|
||||
|
||||
if read := getattr(self.value, 'read', None):
|
||||
if hasattr(self.value, 'read'):
|
||||
try:
|
||||
value = read(connection) # pylint: disable=not-callable
|
||||
value = self.value.read(connection)
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
@@ -815,7 +841,7 @@ class Attribute(EventEmitter):
|
||||
|
||||
return self.encode_value(value)
|
||||
|
||||
def write_value(self, connection: Connection, value_bytes):
|
||||
def write_value(self, connection: Connection, value_bytes: bytes) -> None:
|
||||
if (
|
||||
self.permissions & self.WRITE_REQUIRES_ENCRYPTION
|
||||
) and not connection.encryption:
|
||||
@@ -836,9 +862,9 @@ class Attribute(EventEmitter):
|
||||
|
||||
value = self.decode_value(value_bytes)
|
||||
|
||||
if write := getattr(self.value, 'write', None):
|
||||
if hasattr(self.value, 'write'):
|
||||
try:
|
||||
write(connection, value) # pylint: disable=not-callable
|
||||
self.value.write(connection, value) # pylint: disable=not-callable
|
||||
except ATT_Error as error:
|
||||
raise ATT_Error(
|
||||
error_code=error.error_code, att_handle=self.handle
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import itertools
|
||||
@@ -58,8 +60,10 @@ from bumble.hci import (
|
||||
HCI_Packet,
|
||||
HCI_Role_Change_Event,
|
||||
)
|
||||
from typing import Optional, Union, Dict
|
||||
from typing import Optional, Union, Dict, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.transport.common import TransportSink, TransportSource
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -104,7 +108,7 @@ class Controller:
|
||||
self,
|
||||
name,
|
||||
host_source=None,
|
||||
host_sink=None,
|
||||
host_sink: Optional[TransportSink] = None,
|
||||
link=None,
|
||||
public_address: Optional[Union[bytes, str, Address]] = None,
|
||||
):
|
||||
@@ -188,6 +192,8 @@ class Controller:
|
||||
if link:
|
||||
link.add_controller(self)
|
||||
|
||||
self.terminated = asyncio.get_running_loop().create_future()
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self.hci_sink
|
||||
@@ -288,10 +294,9 @@ class Controller:
|
||||
if self.host:
|
||||
self.host.on_packet(packet.to_bytes())
|
||||
|
||||
# This method allow the controller to emulate the same API as a transport source
|
||||
# This method allows the controller to emulate the same API as a transport source
|
||||
async def wait_for_termination(self):
|
||||
# For now, just wait forever
|
||||
await asyncio.get_running_loop().create_future()
|
||||
await self.terminated
|
||||
|
||||
############################################################
|
||||
# Link connections
|
||||
@@ -654,7 +659,7 @@ class Controller:
|
||||
|
||||
def on_hci_create_connection_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.1.5 Create Connection command
|
||||
See Bluetooth spec Vol 4, Part E - 7.1.5 Create Connection command
|
||||
'''
|
||||
|
||||
if self.link is None:
|
||||
@@ -685,7 +690,7 @@ class Controller:
|
||||
|
||||
def on_hci_disconnect_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.1.6 Disconnect Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.1.6 Disconnect Command
|
||||
'''
|
||||
# First, say that the disconnection is pending
|
||||
self.send_hci_packet(
|
||||
@@ -719,7 +724,7 @@ class Controller:
|
||||
|
||||
def on_hci_accept_connection_request_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.1.8 Accept Connection Request command
|
||||
See Bluetooth spec Vol 4, Part E - 7.1.8 Accept Connection Request command
|
||||
'''
|
||||
|
||||
if self.link is None:
|
||||
@@ -735,7 +740,7 @@ class Controller:
|
||||
|
||||
def on_hci_switch_role_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.2.8 Switch Role command
|
||||
See Bluetooth spec Vol 4, Part E - 7.2.8 Switch Role command
|
||||
'''
|
||||
|
||||
if self.link is None:
|
||||
@@ -751,21 +756,21 @@ class Controller:
|
||||
|
||||
def on_hci_set_event_mask_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.1 Set Event Mask Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.1 Set Event Mask Command
|
||||
'''
|
||||
self.event_mask = command.event_mask
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_reset_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.2 Reset Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.2 Reset Command
|
||||
'''
|
||||
# TODO: cleanup what needs to be reset
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_write_local_name_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.11 Write Local Name Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.11 Write Local Name Command
|
||||
'''
|
||||
local_name = command.local_name
|
||||
if len(local_name):
|
||||
@@ -780,7 +785,7 @@ class Controller:
|
||||
|
||||
def on_hci_read_local_name_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.12 Read Local Name Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.12 Read Local Name Command
|
||||
'''
|
||||
local_name = bytes(self.local_name, 'utf-8')[:248]
|
||||
if len(local_name) < 248:
|
||||
@@ -790,19 +795,19 @@ class Controller:
|
||||
|
||||
def on_hci_read_class_of_device_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.25 Read Class of Device Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.25 Read Class of Device Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, 0, 0, 0])
|
||||
|
||||
def on_hci_write_class_of_device_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.26 Write Class of Device Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.26 Write Class of Device Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_read_synchronous_flow_control_enable_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.36 Read Synchronous Flow Control Enable
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.36 Read Synchronous Flow Control Enable
|
||||
Command
|
||||
'''
|
||||
if self.sync_flow_control:
|
||||
@@ -813,7 +818,7 @@ class Controller:
|
||||
|
||||
def on_hci_write_synchronous_flow_control_enable_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.37 Write Synchronous Flow Control Enable
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.37 Write Synchronous Flow Control Enable
|
||||
Command
|
||||
'''
|
||||
ret = HCI_SUCCESS
|
||||
@@ -825,41 +830,59 @@ class Controller:
|
||||
ret = HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR
|
||||
return bytes([ret])
|
||||
|
||||
def on_hci_set_controller_to_host_flow_control_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.38 Set Controller To Host Flow Control
|
||||
Command
|
||||
'''
|
||||
# For now we just accept the command but ignore the values.
|
||||
# TODO: respect the passed in values.
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_host_buffer_size_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.39 Host Buffer Size Command
|
||||
'''
|
||||
# For now we just accept the command but ignore the values.
|
||||
# TODO: respect the passed in values.
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_write_extended_inquiry_response_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.59 Write Simple Pairing Mode Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.56 Write Extended Inquiry Response
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_write_simple_pairing_mode_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.59 Write Simple Pairing Mode Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.59 Write Simple Pairing Mode Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_set_event_mask_page_2_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.69 Set Event Mask Page 2 Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.69 Set Event Mask Page 2 Command
|
||||
'''
|
||||
self.event_mask_page_2 = command.event_mask_page_2
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_read_le_host_support_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.78 Write LE Host Support Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.78 Write LE Host Support Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, 1, 0])
|
||||
|
||||
def on_hci_write_le_host_support_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.79 Write LE Host Support Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.79 Write LE Host Support Command
|
||||
'''
|
||||
# TODO / Just ignore for now
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_write_authenticated_payload_timeout_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.3.94 Write Authenticated Payload Timeout
|
||||
See Bluetooth spec Vol 4, Part E - 7.3.94 Write Authenticated Payload Timeout
|
||||
Command
|
||||
'''
|
||||
# TODO
|
||||
@@ -867,7 +890,7 @@ class Controller:
|
||||
|
||||
def on_hci_read_local_version_information_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.4.1 Read Local Version Information Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.4.1 Read Local Version Information Command
|
||||
'''
|
||||
return struct.pack(
|
||||
'<BBHBHH',
|
||||
@@ -881,19 +904,19 @@ class Controller:
|
||||
|
||||
def on_hci_read_local_supported_commands_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.4.2 Read Local Supported Commands Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.4.2 Read Local Supported Commands Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.supported_commands
|
||||
|
||||
def on_hci_read_local_supported_features_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.4.3 Read Local Supported Features Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.4.3 Read Local Supported Features Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.lmp_features
|
||||
|
||||
def on_hci_read_bd_addr_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.4.6 Read BD_ADDR Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.4.6 Read BD_ADDR Command
|
||||
'''
|
||||
bd_addr = (
|
||||
self._public_address.to_bytes()
|
||||
@@ -904,14 +927,14 @@ class Controller:
|
||||
|
||||
def on_hci_le_set_event_mask_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.1 LE Set Event Mask Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.1 LE Set Event Mask Command
|
||||
'''
|
||||
self.le_event_mask = command.le_event_mask
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_buffer_size_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.2 LE Read Buffer Size Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.2 LE Read Buffer Size Command
|
||||
'''
|
||||
return struct.pack(
|
||||
'<BHB',
|
||||
@@ -922,49 +945,49 @@ class Controller:
|
||||
|
||||
def on_hci_le_read_local_supported_features_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.3 LE Read Local Supported Features
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.3 LE Read Local Supported Features
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.le_features
|
||||
|
||||
def on_hci_le_set_random_address_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.4 LE Set Random Address Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.4 LE Set Random Address Command
|
||||
'''
|
||||
self.random_address = command.random_address
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_set_advertising_parameters_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.5 LE Set Advertising Parameters Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.5 LE Set Advertising Parameters Command
|
||||
'''
|
||||
self.advertising_parameters = command
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_advertising_physical_channel_tx_power_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.6 LE Read Advertising Physical Channel
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.6 LE Read Advertising Physical Channel
|
||||
Tx Power Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, self.advertising_channel_tx_power])
|
||||
|
||||
def on_hci_le_set_advertising_data_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.7 LE Set Advertising Data Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.7 LE Set Advertising Data Command
|
||||
'''
|
||||
self.advertising_data = command.advertising_data
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_set_scan_response_data_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.8 LE Set Scan Response Data Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.8 LE Set Scan Response Data Command
|
||||
'''
|
||||
self.le_scan_response_data = command.scan_response_data
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_set_advertising_enable_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.9 LE Set Advertising Enable Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.9 LE Set Advertising Enable Command
|
||||
'''
|
||||
if command.advertising_enable:
|
||||
self.start_advertising()
|
||||
@@ -975,7 +998,7 @@ class Controller:
|
||||
|
||||
def on_hci_le_set_scan_parameters_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.10 LE Set Scan Parameters Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.10 LE Set Scan Parameters Command
|
||||
'''
|
||||
self.le_scan_type = command.le_scan_type
|
||||
self.le_scan_interval = command.le_scan_interval
|
||||
@@ -986,7 +1009,7 @@ class Controller:
|
||||
|
||||
def on_hci_le_set_scan_enable_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.11 LE Set Scan Enable Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.11 LE Set Scan Enable Command
|
||||
'''
|
||||
self.le_scan_enable = command.le_scan_enable
|
||||
self.filter_duplicates = command.filter_duplicates
|
||||
@@ -994,7 +1017,7 @@ class Controller:
|
||||
|
||||
def on_hci_le_create_connection_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.12 LE Create Connection Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.12 LE Create Connection Command
|
||||
'''
|
||||
|
||||
if not self.link:
|
||||
@@ -1027,40 +1050,40 @@ class Controller:
|
||||
|
||||
def on_hci_le_create_connection_cancel_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.13 LE Create Connection Cancel Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.13 LE Create Connection Cancel Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_filter_accept_list_size_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.14 LE Read Filter Accept List Size
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.14 LE Read Filter Accept List Size
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, self.filter_accept_list_size])
|
||||
|
||||
def on_hci_le_clear_filter_accept_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.15 LE Clear Filter Accept List Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.15 LE Clear Filter Accept List Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_add_device_to_filter_accept_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.16 LE Add Device To Filter Accept List
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.16 LE Add Device To Filter Accept List
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_remove_device_from_filter_accept_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.17 LE Remove Device From Filter Accept
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.17 LE Remove Device From Filter Accept
|
||||
List Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_remote_features_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.21 LE Read Remote Features Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.21 LE Read Remote Features Command
|
||||
'''
|
||||
|
||||
# First, say that the command is pending
|
||||
@@ -1083,13 +1106,13 @@ class Controller:
|
||||
|
||||
def on_hci_le_rand_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.23 LE Rand Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.23 LE Rand Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + struct.pack('Q', random.randint(0, 1 << 64))
|
||||
|
||||
def on_hci_le_enable_encryption_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.24 LE Enable Encryption Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.24 LE Enable Encryption Command
|
||||
'''
|
||||
|
||||
# Check the parameters
|
||||
@@ -1122,13 +1145,13 @@ class Controller:
|
||||
|
||||
def on_hci_le_read_supported_states_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.27 LE Read Supported States Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.27 LE Read Supported States Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS]) + self.le_states
|
||||
|
||||
def on_hci_le_read_suggested_default_data_length_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.34 LE Read Suggested Default Data Length
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.34 LE Read Suggested Default Data Length
|
||||
Command
|
||||
'''
|
||||
return struct.pack(
|
||||
@@ -1140,7 +1163,7 @@ class Controller:
|
||||
|
||||
def on_hci_le_write_suggested_default_data_length_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.35 LE Write Suggested Default Data Length
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.35 LE Write Suggested Default Data Length
|
||||
Command
|
||||
'''
|
||||
self.suggested_max_tx_octets, self.suggested_max_tx_time = struct.unpack(
|
||||
@@ -1150,33 +1173,33 @@ class Controller:
|
||||
|
||||
def on_hci_le_read_local_p_256_public_key_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.36 LE Read P-256 Public Key Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.36 LE Read P-256 Public Key Command
|
||||
'''
|
||||
# TODO create key and send HCI_LE_Read_Local_P-256_Public_Key_Complete event
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_add_device_to_resolving_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.38 LE Add Device To Resolving List
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.38 LE Add Device To Resolving List
|
||||
Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_clear_resolving_list_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.40 LE Clear Resolving List Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.40 LE Clear Resolving List Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS])
|
||||
|
||||
def on_hci_le_read_resolving_list_size_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.41 LE Read Resolving List Size Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.41 LE Read Resolving List Size Command
|
||||
'''
|
||||
return bytes([HCI_SUCCESS, self.resolving_list_size])
|
||||
|
||||
def on_hci_le_set_address_resolution_enable_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.44 LE Set Address Resolution Enable
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.44 LE Set Address Resolution Enable
|
||||
Command
|
||||
'''
|
||||
ret = HCI_SUCCESS
|
||||
@@ -1190,7 +1213,7 @@ class Controller:
|
||||
|
||||
def on_hci_le_set_resolvable_private_address_timeout_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.45 LE Set Resolvable Private Address
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.45 LE Set Resolvable Private Address
|
||||
Timeout Command
|
||||
'''
|
||||
self.le_rpa_timeout = command.rpa_timeout
|
||||
@@ -1198,7 +1221,7 @@ class Controller:
|
||||
|
||||
def on_hci_le_read_maximum_data_length_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.46 LE Read Maximum Data Length Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.46 LE Read Maximum Data Length Command
|
||||
'''
|
||||
return struct.pack(
|
||||
'<BHHHH',
|
||||
@@ -1211,7 +1234,7 @@ class Controller:
|
||||
|
||||
def on_hci_le_read_phy_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.47 LE Read PHY Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.47 LE Read PHY Command
|
||||
'''
|
||||
return struct.pack(
|
||||
'<BHBB',
|
||||
@@ -1223,7 +1246,7 @@ class Controller:
|
||||
|
||||
def on_hci_le_set_default_phy_command(self, command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.48 LE Set Default PHY Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.48 LE Set Default PHY Command
|
||||
'''
|
||||
self.default_phy = {
|
||||
'all_phys': command.all_phys,
|
||||
@@ -1234,6 +1257,6 @@ class Controller:
|
||||
|
||||
def on_hci_le_read_transmit_power_command(self, _command):
|
||||
'''
|
||||
See Bluetooth spec Vol 2, Part E - 7.8.74 LE Read Transmit Power Command
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.74 LE Read Transmit Power Command
|
||||
'''
|
||||
return struct.pack('<BBB', HCI_SUCCESS, 0, 0)
|
||||
|
||||
101
bumble/core.py
101
bumble/core.py
@@ -17,7 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import struct
|
||||
from typing import List, Optional, Tuple, Union, cast
|
||||
from typing import List, Optional, Tuple, Union, cast, Dict
|
||||
|
||||
from .company_ids import COMPANY_IDENTIFIERS
|
||||
|
||||
@@ -53,7 +53,7 @@ def bit_flags_to_strings(bits, bit_flag_names):
|
||||
return names
|
||||
|
||||
|
||||
def name_or_number(dictionary, number, width=2):
|
||||
def name_or_number(dictionary: Dict[int, str], number: int, width: int = 2) -> str:
|
||||
name = dictionary.get(number)
|
||||
if name is not None:
|
||||
return name
|
||||
@@ -78,7 +78,13 @@ def get_dict_key_by_value(dictionary, value):
|
||||
class BaseError(Exception):
|
||||
"""Base class for errors with an error code, error name and namespace"""
|
||||
|
||||
def __init__(self, error_code, error_namespace='', error_name='', details=''):
|
||||
def __init__(
|
||||
self,
|
||||
error_code: Optional[int],
|
||||
error_namespace: str = '',
|
||||
error_name: str = '',
|
||||
details: str = '',
|
||||
):
|
||||
super().__init__()
|
||||
self.error_code = error_code
|
||||
self.error_namespace = error_namespace
|
||||
@@ -90,12 +96,14 @@ class BaseError(Exception):
|
||||
namespace = f'{self.error_namespace}/'
|
||||
else:
|
||||
namespace = ''
|
||||
if self.error_name:
|
||||
name = f'{self.error_name} [0x{self.error_code:X}]'
|
||||
else:
|
||||
name = f'0x{self.error_code:X}'
|
||||
error_text = {
|
||||
(True, True): f'{self.error_name} [0x{self.error_code:X}]',
|
||||
(True, False): self.error_name,
|
||||
(False, True): f'0x{self.error_code:X}',
|
||||
(False, False): '',
|
||||
}[(self.error_name != '', self.error_code is not None)]
|
||||
|
||||
return f'{type(self).__name__}({namespace}{name})'
|
||||
return f'{type(self).__name__}({namespace}{error_text})'
|
||||
|
||||
|
||||
class ProtocolError(BaseError):
|
||||
@@ -134,6 +142,10 @@ class ConnectionError(BaseError): # pylint: disable=redefined-builtin
|
||||
self.peer_address = peer_address
|
||||
|
||||
|
||||
class ConnectionParameterUpdateError(BaseError):
|
||||
"""Connection Parameter Update Error"""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# UUID
|
||||
#
|
||||
@@ -562,11 +574,82 @@ class DeviceClass:
|
||||
PERIPHERAL_HANDHELD_GESTURAL_INPUT_DEVICE_MINOR_DEVICE_CLASS: 'Handheld gestural input device'
|
||||
}
|
||||
|
||||
WEARABLE_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
|
||||
WEARABLE_WRISTWATCH_MINOR_DEVICE_CLASS = 0x01
|
||||
WEARABLE_PAGER_MINOR_DEVICE_CLASS = 0x02
|
||||
WEARABLE_JACKET_MINOR_DEVICE_CLASS = 0x03
|
||||
WEARABLE_HELMET_MINOR_DEVICE_CLASS = 0x04
|
||||
WEARABLE_GLASSES_MINOR_DEVICE_CLASS = 0x05
|
||||
|
||||
WEARABLE_MINOR_DEVICE_CLASS_NAMES = {
|
||||
WEARABLE_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
|
||||
WEARABLE_WRISTWATCH_MINOR_DEVICE_CLASS: 'Wristwatch',
|
||||
WEARABLE_PAGER_MINOR_DEVICE_CLASS: 'Pager',
|
||||
WEARABLE_JACKET_MINOR_DEVICE_CLASS: 'Jacket',
|
||||
WEARABLE_HELMET_MINOR_DEVICE_CLASS: 'Helmet',
|
||||
WEARABLE_GLASSES_MINOR_DEVICE_CLASS: 'Glasses',
|
||||
}
|
||||
|
||||
TOY_UNCATEGORIZED_MINOR_DEVICE_CLASS = 0x00
|
||||
TOY_ROBOT_MINOR_DEVICE_CLASS = 0x01
|
||||
TOY_VEHICLE_MINOR_DEVICE_CLASS = 0x02
|
||||
TOY_DOLL_ACTION_FIGURE_MINOR_DEVICE_CLASS = 0x03
|
||||
TOY_CONTROLLER_MINOR_DEVICE_CLASS = 0x04
|
||||
TOY_GAME_MINOR_DEVICE_CLASS = 0x05
|
||||
|
||||
TOY_MINOR_DEVICE_CLASS_NAMES = {
|
||||
TOY_UNCATEGORIZED_MINOR_DEVICE_CLASS: 'Uncategorized',
|
||||
TOY_ROBOT_MINOR_DEVICE_CLASS: 'Robot',
|
||||
TOY_VEHICLE_MINOR_DEVICE_CLASS: 'Vehicle',
|
||||
TOY_DOLL_ACTION_FIGURE_MINOR_DEVICE_CLASS: 'Doll/Action figure',
|
||||
TOY_CONTROLLER_MINOR_DEVICE_CLASS: 'Controller',
|
||||
TOY_GAME_MINOR_DEVICE_CLASS: 'Game',
|
||||
}
|
||||
|
||||
HEALTH_UNDEFINED_MINOR_DEVICE_CLASS = 0x00
|
||||
HEALTH_BLOOD_PRESSURE_MONITOR_MINOR_DEVICE_CLASS = 0x01
|
||||
HEALTH_THERMOMETER_MINOR_DEVICE_CLASS = 0x02
|
||||
HEALTH_WEIGHING_SCALE_MINOR_DEVICE_CLASS = 0x03
|
||||
HEALTH_GLUCOSE_METER_MINOR_DEVICE_CLASS = 0x04
|
||||
HEALTH_PULSE_OXIMETER_MINOR_DEVICE_CLASS = 0x05
|
||||
HEALTH_HEART_PULSE_RATE_MONITOR_MINOR_DEVICE_CLASS = 0x06
|
||||
HEALTH_HEALTH_DATA_DISPLAY_MINOR_DEVICE_CLASS = 0x07
|
||||
HEALTH_STEP_COUNTER_MINOR_DEVICE_CLASS = 0x08
|
||||
HEALTH_BODY_COMPOSITION_ANALYZER_MINOR_DEVICE_CLASS = 0x09
|
||||
HEALTH_PEAK_FLOW_MONITOR_MINOR_DEVICE_CLASS = 0x0A
|
||||
HEALTH_MEDICATION_MONITOR_MINOR_DEVICE_CLASS = 0x0B
|
||||
HEALTH_KNEE_PROSTHESIS_MINOR_DEVICE_CLASS = 0x0C
|
||||
HEALTH_ANKLE_PROSTHESIS_MINOR_DEVICE_CLASS = 0x0D
|
||||
HEALTH_GENERIC_HEALTH_MANAGER_MINOR_DEVICE_CLASS = 0x0E
|
||||
HEALTH_PERSONAL_MOBILITY_DEVICE_MINOR_DEVICE_CLASS = 0x0F
|
||||
|
||||
HEALTH_MINOR_DEVICE_CLASS_NAMES = {
|
||||
HEALTH_UNDEFINED_MINOR_DEVICE_CLASS: 'Undefined',
|
||||
HEALTH_BLOOD_PRESSURE_MONITOR_MINOR_DEVICE_CLASS: 'Blood Pressure Monitor',
|
||||
HEALTH_THERMOMETER_MINOR_DEVICE_CLASS: 'Thermometer',
|
||||
HEALTH_WEIGHING_SCALE_MINOR_DEVICE_CLASS: 'Weighing Scale',
|
||||
HEALTH_GLUCOSE_METER_MINOR_DEVICE_CLASS: 'Glucose Meter',
|
||||
HEALTH_PULSE_OXIMETER_MINOR_DEVICE_CLASS: 'Pulse Oximeter',
|
||||
HEALTH_HEART_PULSE_RATE_MONITOR_MINOR_DEVICE_CLASS: 'Heart/Pulse Rate Monitor',
|
||||
HEALTH_HEALTH_DATA_DISPLAY_MINOR_DEVICE_CLASS: 'Health Data Display',
|
||||
HEALTH_STEP_COUNTER_MINOR_DEVICE_CLASS: 'Step Counter',
|
||||
HEALTH_BODY_COMPOSITION_ANALYZER_MINOR_DEVICE_CLASS: 'Body Composition Analyzer',
|
||||
HEALTH_PEAK_FLOW_MONITOR_MINOR_DEVICE_CLASS: 'Peak Flow Monitor',
|
||||
HEALTH_MEDICATION_MONITOR_MINOR_DEVICE_CLASS: 'Medication Monitor',
|
||||
HEALTH_KNEE_PROSTHESIS_MINOR_DEVICE_CLASS: 'Knee Prosthesis',
|
||||
HEALTH_ANKLE_PROSTHESIS_MINOR_DEVICE_CLASS: 'Ankle Prosthesis',
|
||||
HEALTH_GENERIC_HEALTH_MANAGER_MINOR_DEVICE_CLASS: 'Generic Health Manager',
|
||||
HEALTH_PERSONAL_MOBILITY_DEVICE_MINOR_DEVICE_CLASS: 'Personal Mobility Device',
|
||||
}
|
||||
|
||||
MINOR_DEVICE_CLASS_NAMES = {
|
||||
COMPUTER_MAJOR_DEVICE_CLASS: COMPUTER_MINOR_DEVICE_CLASS_NAMES,
|
||||
PHONE_MAJOR_DEVICE_CLASS: PHONE_MINOR_DEVICE_CLASS_NAMES,
|
||||
AUDIO_VIDEO_MAJOR_DEVICE_CLASS: AUDIO_VIDEO_MINOR_DEVICE_CLASS_NAMES,
|
||||
PERIPHERAL_MAJOR_DEVICE_CLASS: PERIPHERAL_MINOR_DEVICE_CLASS_NAMES
|
||||
PERIPHERAL_MAJOR_DEVICE_CLASS: PERIPHERAL_MINOR_DEVICE_CLASS_NAMES,
|
||||
WEARABLE_MAJOR_DEVICE_CLASS: WEARABLE_MINOR_DEVICE_CLASS_NAMES,
|
||||
TOY_MAJOR_DEVICE_CLASS: TOY_MINOR_DEVICE_CLASS_NAMES,
|
||||
HEALTH_MAJOR_DEVICE_CLASS: HEALTH_MINOR_DEVICE_CLASS_NAMES,
|
||||
}
|
||||
|
||||
# fmt: on
|
||||
|
||||
@@ -23,22 +23,18 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import operator
|
||||
import platform
|
||||
|
||||
if platform.system() != 'Emscripten':
|
||||
import secrets
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
generate_private_key,
|
||||
ECDH,
|
||||
EllipticCurvePublicNumbers,
|
||||
EllipticCurvePrivateNumbers,
|
||||
SECP256R1,
|
||||
)
|
||||
from cryptography.hazmat.primitives import cmac
|
||||
else:
|
||||
# TODO: implement stubs
|
||||
pass
|
||||
import secrets
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
generate_private_key,
|
||||
ECDH,
|
||||
EllipticCurvePublicNumbers,
|
||||
EllipticCurvePrivateNumbers,
|
||||
SECP256R1,
|
||||
)
|
||||
from cryptography.hazmat.primitives import cmac
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
|
||||
276
bumble/device.py
276
bumble/device.py
@@ -23,7 +23,18 @@ import asyncio
|
||||
import logging
|
||||
from contextlib import asynccontextmanager, AsyncExitStack
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, ClassVar, Dict, List, Optional, Tuple, Type, Union
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from .colors import color
|
||||
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
||||
@@ -86,6 +97,7 @@ from .hci import (
|
||||
HCI_LE_Extended_Create_Connection_Command,
|
||||
HCI_LE_Rand_Command,
|
||||
HCI_LE_Read_PHY_Command,
|
||||
HCI_LE_Set_Address_Resolution_Enable_Command,
|
||||
HCI_LE_Set_Advertising_Data_Command,
|
||||
HCI_LE_Set_Advertising_Enable_Command,
|
||||
HCI_LE_Set_Advertising_Parameters_Command,
|
||||
@@ -129,6 +141,7 @@ from .core import (
|
||||
BT_LE_TRANSPORT,
|
||||
BT_PERIPHERAL_ROLE,
|
||||
AdvertisingData,
|
||||
ConnectionParameterUpdateError,
|
||||
CommandTimeoutError,
|
||||
ConnectionPHY,
|
||||
InvalidStateError,
|
||||
@@ -151,6 +164,9 @@ from . import sdp
|
||||
from . import l2cap
|
||||
from . import core
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .transport.common import TransportSource, TransportSink
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -651,7 +667,7 @@ class Connection(CompositeEventEmitter):
|
||||
def is_incomplete(self) -> bool:
|
||||
return self.handle is None
|
||||
|
||||
def send_l2cap_pdu(self, cid, pdu):
|
||||
def send_l2cap_pdu(self, cid: int, pdu: bytes) -> None:
|
||||
self.device.send_l2cap_pdu(self.handle, cid, pdu)
|
||||
|
||||
def create_l2cap_connector(self, psm):
|
||||
@@ -708,6 +724,7 @@ class Connection(CompositeEventEmitter):
|
||||
connection_interval_max,
|
||||
max_latency,
|
||||
supervision_timeout,
|
||||
use_l2cap=False,
|
||||
):
|
||||
return await self.device.update_connection_parameters(
|
||||
self,
|
||||
@@ -715,6 +732,7 @@ class Connection(CompositeEventEmitter):
|
||||
connection_interval_max,
|
||||
max_latency,
|
||||
supervision_timeout,
|
||||
use_l2cap=use_l2cap,
|
||||
)
|
||||
|
||||
async def set_phy(self, tx_phys=None, rx_phys=None, phy_options=None):
|
||||
@@ -778,6 +796,7 @@ class DeviceConfiguration:
|
||||
self.irk = bytes(16) # This really must be changed for any level of security
|
||||
self.keystore = None
|
||||
self.gatt_services: List[Dict[str, Any]] = []
|
||||
self.address_resolution_offload = False
|
||||
|
||||
def load_from_dict(self, config: Dict[str, Any]) -> None:
|
||||
# Load simple properties
|
||||
@@ -940,7 +959,13 @@ class Device(CompositeEventEmitter):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def with_hci(cls, name, address, hci_source, hci_sink):
|
||||
def with_hci(
|
||||
cls,
|
||||
name: str,
|
||||
address: Address,
|
||||
hci_source: TransportSource,
|
||||
hci_sink: TransportSink,
|
||||
) -> Device:
|
||||
'''
|
||||
Create a Device instance with a Host configured to communicate with a controller
|
||||
through an HCI source/sink
|
||||
@@ -949,18 +974,25 @@ class Device(CompositeEventEmitter):
|
||||
return cls(name=name, address=address, host=host)
|
||||
|
||||
@classmethod
|
||||
def from_config_file(cls, filename):
|
||||
def from_config_file(cls, filename: str) -> Device:
|
||||
config = DeviceConfiguration()
|
||||
config.load_from_file(filename)
|
||||
return cls(config=config)
|
||||
|
||||
@classmethod
|
||||
def from_config_with_hci(cls, config, hci_source, hci_sink):
|
||||
def from_config_with_hci(
|
||||
cls,
|
||||
config: DeviceConfiguration,
|
||||
hci_source: TransportSource,
|
||||
hci_sink: TransportSink,
|
||||
) -> Device:
|
||||
host = Host(controller_source=hci_source, controller_sink=hci_sink)
|
||||
return cls(config=config, host=host)
|
||||
|
||||
@classmethod
|
||||
def from_config_file_with_hci(cls, filename, hci_source, hci_sink):
|
||||
def from_config_file_with_hci(
|
||||
cls, filename: str, hci_source: TransportSource, hci_sink: TransportSink
|
||||
) -> Device:
|
||||
config = DeviceConfiguration()
|
||||
config.load_from_file(filename)
|
||||
return cls.from_config_with_hci(config, hci_source, hci_sink)
|
||||
@@ -1029,6 +1061,7 @@ class Device(CompositeEventEmitter):
|
||||
self.discoverable = config.discoverable
|
||||
self.connectable = config.connectable
|
||||
self.classic_accept_any = config.classic_accept_any
|
||||
self.address_resolution_offload = config.address_resolution_offload
|
||||
|
||||
for service in config.gatt_services:
|
||||
characteristics = []
|
||||
@@ -1093,7 +1126,7 @@ class Device(CompositeEventEmitter):
|
||||
return self._host
|
||||
|
||||
@host.setter
|
||||
def host(self, host):
|
||||
def host(self, host: Host) -> None:
|
||||
# Unsubscribe from events from the current host
|
||||
if self._host:
|
||||
for event_name in device_host_event_handlers:
|
||||
@@ -1153,8 +1186,8 @@ class Device(CompositeEventEmitter):
|
||||
def create_l2cap_registrar(self, psm):
|
||||
return lambda handler: self.register_l2cap_server(psm, handler)
|
||||
|
||||
def register_l2cap_server(self, psm, server):
|
||||
self.l2cap_channel_manager.register_server(psm, server)
|
||||
def register_l2cap_server(self, psm, server) -> int:
|
||||
return self.l2cap_channel_manager.register_server(psm, server)
|
||||
|
||||
def register_l2cap_channel_server(
|
||||
self,
|
||||
@@ -1180,7 +1213,7 @@ class Device(CompositeEventEmitter):
|
||||
connection, psm, max_credits, mtu, mps
|
||||
)
|
||||
|
||||
def send_l2cap_pdu(self, connection_handle, cid, pdu):
|
||||
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
||||
self.host.send_l2cap_pdu(connection_handle, cid, pdu)
|
||||
|
||||
async def send_command(self, command, check_result=False):
|
||||
@@ -1256,31 +1289,16 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
|
||||
# Load the address resolving list
|
||||
if self.keystore and self.host.supports_command(
|
||||
HCI_LE_CLEAR_RESOLVING_LIST_COMMAND
|
||||
):
|
||||
await self.send_command(HCI_LE_Clear_Resolving_List_Command()) # type: ignore[call-arg]
|
||||
if self.keystore:
|
||||
await self.refresh_resolving_list()
|
||||
|
||||
resolving_keys = await self.keystore.get_resolving_keys()
|
||||
for irk, address in resolving_keys:
|
||||
await self.send_command(
|
||||
HCI_LE_Add_Device_To_Resolving_List_Command(
|
||||
peer_identity_address_type=address.address_type,
|
||||
peer_identity_address=address,
|
||||
peer_irk=irk,
|
||||
local_irk=self.irk,
|
||||
) # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
# Enable address resolution
|
||||
# await self.send_command(
|
||||
# HCI_LE_Set_Address_Resolution_Enable_Command(
|
||||
# address_resolution_enable=1)
|
||||
# )
|
||||
# )
|
||||
|
||||
# Create a host-side address resolver
|
||||
self.address_resolver = smp.AddressResolver(resolving_keys)
|
||||
# Enable address resolution
|
||||
if self.address_resolution_offload:
|
||||
await self.send_command(
|
||||
HCI_LE_Set_Address_Resolution_Enable_Command(
|
||||
address_resolution_enable=1
|
||||
) # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
if self.classic_enabled:
|
||||
await self.send_command(
|
||||
@@ -1310,6 +1328,26 @@ class Device(CompositeEventEmitter):
|
||||
await self.host.flush()
|
||||
self.powered_on = False
|
||||
|
||||
async def refresh_resolving_list(self) -> None:
|
||||
assert self.keystore is not None
|
||||
|
||||
resolving_keys = await self.keystore.get_resolving_keys()
|
||||
# Create a host-side address resolver
|
||||
self.address_resolver = smp.AddressResolver(resolving_keys)
|
||||
|
||||
if self.address_resolution_offload:
|
||||
await self.send_command(HCI_LE_Clear_Resolving_List_Command()) # type: ignore[call-arg]
|
||||
|
||||
for irk, address in resolving_keys:
|
||||
await self.send_command(
|
||||
HCI_LE_Add_Device_To_Resolving_List_Command(
|
||||
peer_identity_address_type=address.address_type,
|
||||
peer_identity_address=address,
|
||||
peer_irk=irk,
|
||||
local_irk=self.irk,
|
||||
) # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
def supports_le_feature(self, feature):
|
||||
return self.host.supports_le_feature(feature)
|
||||
|
||||
@@ -1387,10 +1425,10 @@ class Device(CompositeEventEmitter):
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
self.advertising_own_address_type = own_address_type
|
||||
self.auto_restart_advertising = auto_restart
|
||||
self.advertising_type = advertising_type
|
||||
self.advertising_own_address_type = own_address_type
|
||||
self.advertising = True
|
||||
self.auto_restart_advertising = auto_restart
|
||||
|
||||
async def stop_advertising(self) -> None:
|
||||
# Disable advertising
|
||||
@@ -1400,9 +1438,9 @@ class Device(CompositeEventEmitter):
|
||||
check_result=True,
|
||||
)
|
||||
|
||||
self.advertising_type = None
|
||||
self.advertising_own_address_type = None
|
||||
self.advertising = False
|
||||
self.advertising_type = None
|
||||
self.auto_restart_advertising = False
|
||||
|
||||
@property
|
||||
@@ -2075,11 +2113,30 @@ class Device(CompositeEventEmitter):
|
||||
supervision_timeout,
|
||||
min_ce_length=0,
|
||||
max_ce_length=0,
|
||||
):
|
||||
use_l2cap=False,
|
||||
) -> None:
|
||||
'''
|
||||
NOTE: the name of the parameters may look odd, but it just follows the names
|
||||
used in the Bluetooth spec.
|
||||
'''
|
||||
|
||||
if use_l2cap:
|
||||
if connection.role != BT_PERIPHERAL_ROLE:
|
||||
raise InvalidStateError(
|
||||
'only peripheral can update connection parameters with l2cap'
|
||||
)
|
||||
l2cap_result = (
|
||||
await self.l2cap_channel_manager.update_connection_parameters(
|
||||
connection,
|
||||
connection_interval_min,
|
||||
connection_interval_max,
|
||||
max_latency,
|
||||
supervision_timeout,
|
||||
)
|
||||
)
|
||||
if l2cap_result != l2cap.L2CAP_CONNECTION_PARAMETERS_ACCEPTED_RESULT:
|
||||
raise ConnectionParameterUpdateError(l2cap_result)
|
||||
|
||||
result = await self.send_command(
|
||||
HCI_LE_Connection_Update_Command(
|
||||
connection_handle=connection.handle,
|
||||
@@ -2089,7 +2146,7 @@ class Device(CompositeEventEmitter):
|
||||
supervision_timeout=supervision_timeout,
|
||||
min_ce_length=min_ce_length,
|
||||
max_ce_length=max_ce_length,
|
||||
)
|
||||
) # type: ignore[call-arg]
|
||||
)
|
||||
if result.status != HCI_Command_Status_Event.PENDING:
|
||||
raise HCI_StatusError(result)
|
||||
@@ -2230,9 +2287,11 @@ class Device(CompositeEventEmitter):
|
||||
def request_pairing(self, connection):
|
||||
return self.smp_manager.request_pairing(connection)
|
||||
|
||||
async def get_long_term_key(self, connection_handle, rand, ediv):
|
||||
async def get_long_term_key(
|
||||
self, connection_handle: int, rand: bytes, ediv: int
|
||||
) -> Optional[bytes]:
|
||||
if (connection := self.lookup_connection(connection_handle)) is None:
|
||||
return
|
||||
return None
|
||||
|
||||
# Start by looking for the key in an SMP session
|
||||
ltk = self.smp_manager.get_long_term_key(connection, rand, ediv)
|
||||
@@ -2252,19 +2311,24 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
if connection.role == BT_PERIPHERAL_ROLE and keys.ltk_peripheral:
|
||||
return keys.ltk_peripheral.value
|
||||
return None
|
||||
|
||||
async def get_link_key(self, address: Address) -> Optional[bytes]:
|
||||
# Look for the key in the keystore
|
||||
if self.keystore is not None:
|
||||
keys = await self.keystore.get(str(address))
|
||||
if keys is not None:
|
||||
logger.debug('found keys in the key store')
|
||||
if keys.link_key is None:
|
||||
logger.warning('no link key')
|
||||
return None
|
||||
if self.keystore is None:
|
||||
return None
|
||||
|
||||
return keys.link_key.value
|
||||
return None
|
||||
# Look for the key in the keystore
|
||||
keys = await self.keystore.get(str(address))
|
||||
if keys is None:
|
||||
logger.debug(f'no keys found for {address}')
|
||||
return None
|
||||
|
||||
logger.debug('found keys in the key store')
|
||||
if keys.link_key is None:
|
||||
logger.warning('no link key')
|
||||
return None
|
||||
|
||||
return keys.link_key.value
|
||||
|
||||
# [Classic only]
|
||||
async def authenticate(self, connection):
|
||||
@@ -2383,6 +2447,18 @@ class Device(CompositeEventEmitter):
|
||||
'connection_encryption_failure', on_encryption_failure
|
||||
)
|
||||
|
||||
async def update_keys(self, address: str, keys: PairingKeys) -> None:
|
||||
if self.keystore is None:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.keystore.update(address, keys)
|
||||
await self.refresh_resolving_list()
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! error while storing keys: {error}')
|
||||
else:
|
||||
self.emit('key_store_update')
|
||||
|
||||
# [Classic only]
|
||||
async def switch_role(self, connection: Connection, role: int):
|
||||
pending_role_change = asyncio.get_running_loop().create_future()
|
||||
@@ -2477,13 +2553,7 @@ class Device(CompositeEventEmitter):
|
||||
value=link_key, authenticated=authenticated
|
||||
)
|
||||
|
||||
async def store_keys():
|
||||
try:
|
||||
await self.keystore.update(str(bd_addr), pairing_keys)
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! error while storing keys: {error}')
|
||||
|
||||
self.abort_on('flush', store_keys())
|
||||
self.abort_on('flush', self.update_keys(str(bd_addr), pairing_keys))
|
||||
|
||||
if connection := self.find_connection_by_bd_addr(
|
||||
bd_addr, transport=BT_BR_EDR_TRANSPORT
|
||||
@@ -2560,7 +2630,6 @@ class Device(CompositeEventEmitter):
|
||||
own_address_type = self.advertising_own_address_type
|
||||
|
||||
# We are no longer advertising
|
||||
self.advertising_own_address_type = None
|
||||
self.advertising = False
|
||||
|
||||
if own_address_type in (
|
||||
@@ -2617,7 +2686,6 @@ class Device(CompositeEventEmitter):
|
||||
and self.advertising
|
||||
and self.advertising_type.is_directed
|
||||
):
|
||||
self.advertising_own_address_type = None
|
||||
self.advertising = False
|
||||
|
||||
# Notify listeners
|
||||
@@ -2688,7 +2756,9 @@ class Device(CompositeEventEmitter):
|
||||
self.abort_on(
|
||||
'flush',
|
||||
self.start_advertising(
|
||||
advertising_type=self.advertising_type, auto_restart=True
|
||||
advertising_type=self.advertising_type,
|
||||
own_address_type=self.advertising_own_address_type,
|
||||
auto_restart=True,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2735,20 +2805,6 @@ class Device(CompositeEventEmitter):
|
||||
)
|
||||
connection.emit('connection_authentication_failure', error)
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_ssp_complete(self, connection):
|
||||
# On Secure Simple Pairing complete, in case:
|
||||
# - Connection isn't already authenticated
|
||||
# - AND we are not the initiator of the authentication
|
||||
# We must trigger authentication to know if we are truly authenticated
|
||||
if not connection.authenticating and not connection.authenticated:
|
||||
logger.debug(
|
||||
f'*** Trigger Connection Authentication: [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address}'
|
||||
)
|
||||
asyncio.create_task(connection.authenticate())
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
@@ -2851,18 +2907,22 @@ class Device(CompositeEventEmitter):
|
||||
method = methods[peer_io_capability][io_capability]
|
||||
|
||||
async def reply() -> None:
|
||||
if await connection.abort_on('disconnection', method()):
|
||||
await self.host.send_command(
|
||||
HCI_User_Confirmation_Request_Reply_Command( # type: ignore[call-arg]
|
||||
bd_addr=connection.peer_address
|
||||
)
|
||||
)
|
||||
else:
|
||||
await self.host.send_command(
|
||||
HCI_User_Confirmation_Request_Negative_Reply_Command( # type: ignore[call-arg]
|
||||
bd_addr=connection.peer_address
|
||||
try:
|
||||
if await connection.abort_on('disconnection', method()):
|
||||
await self.host.send_command(
|
||||
HCI_User_Confirmation_Request_Reply_Command( # type: ignore[call-arg]
|
||||
bd_addr=connection.peer_address
|
||||
)
|
||||
)
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while confirming: {error}')
|
||||
|
||||
await self.host.send_command(
|
||||
HCI_User_Confirmation_Request_Negative_Reply_Command( # type: ignore[call-arg]
|
||||
bd_addr=connection.peer_address
|
||||
)
|
||||
)
|
||||
|
||||
AsyncRunner.spawn(reply())
|
||||
|
||||
@@ -2874,21 +2934,25 @@ class Device(CompositeEventEmitter):
|
||||
pairing_config = self.pairing_config_factory(connection)
|
||||
|
||||
async def reply() -> None:
|
||||
number = await connection.abort_on(
|
||||
'disconnection', pairing_config.delegate.get_number()
|
||||
try:
|
||||
number = await connection.abort_on(
|
||||
'disconnection', pairing_config.delegate.get_number()
|
||||
)
|
||||
if number is not None:
|
||||
await self.host.send_command(
|
||||
HCI_User_Passkey_Request_Reply_Command( # type: ignore[call-arg]
|
||||
bd_addr=connection.peer_address, numeric_value=number
|
||||
)
|
||||
)
|
||||
return
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while asking for pass-key: {error}')
|
||||
|
||||
await self.host.send_command(
|
||||
HCI_User_Passkey_Request_Negative_Reply_Command( # type: ignore[call-arg]
|
||||
bd_addr=connection.peer_address
|
||||
)
|
||||
)
|
||||
if number is not None:
|
||||
await self.host.send_command(
|
||||
HCI_User_Passkey_Request_Reply_Command( # type: ignore[call-arg]
|
||||
bd_addr=connection.peer_address, numeric_value=number
|
||||
)
|
||||
)
|
||||
else:
|
||||
await self.host.send_command(
|
||||
HCI_User_Passkey_Request_Negative_Reply_Command( # type: ignore[call-arg]
|
||||
bd_addr=connection.peer_address
|
||||
)
|
||||
)
|
||||
|
||||
AsyncRunner.spawn(reply())
|
||||
|
||||
@@ -3095,6 +3159,18 @@ class Device(CompositeEventEmitter):
|
||||
connection.emit('role_change_failure', error)
|
||||
self.emit('role_change_failure', address, error)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_classic_pairing(self, connection: Connection) -> None:
|
||||
connection.emit('classic_pairing')
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@with_connection_from_address
|
||||
def on_classic_pairing_failure(self, connection: Connection, status) -> None:
|
||||
connection.emit('classic_pairing_failure', status)
|
||||
|
||||
def on_pairing_start(self, connection: Connection) -> None:
|
||||
connection.emit('pairing_start')
|
||||
|
||||
@@ -3143,7 +3219,7 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
@host_event_handler
|
||||
@with_connection_from_handle
|
||||
def on_l2cap_pdu(self, connection, cid, pdu):
|
||||
def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes):
|
||||
self.l2cap_channel_manager.on_pdu(connection, cid, pdu)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
91
bumble/drivers/__init__.py
Normal file
91
bumble/drivers/__init__.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Drivers that can be used to customize the interaction between a host and a controller,
|
||||
like loading firmware after a cold start.
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import abc
|
||||
import logging
|
||||
import pathlib
|
||||
import platform
|
||||
from . import rtk
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
class Driver(abc.ABC):
|
||||
"""Base class for drivers."""
|
||||
|
||||
@staticmethod
|
||||
async def for_host(_host):
|
||||
"""Return a driver instance for a host.
|
||||
|
||||
Args:
|
||||
host: Host object for which a driver should be created.
|
||||
|
||||
Returns:
|
||||
A Driver instance if a driver should be instantiated for this host, or
|
||||
None if no driver instance of this class is needed.
|
||||
"""
|
||||
return None
|
||||
|
||||
@abc.abstractmethod
|
||||
async def init_controller(self):
|
||||
"""Initialize the controller."""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
async def get_driver_for_host(host):
|
||||
"""Probe all known diver classes until one returns a valid instance for a host,
|
||||
or none is found.
|
||||
"""
|
||||
if driver := await rtk.Driver.for_host(host):
|
||||
logger.debug("Instantiated RTK driver")
|
||||
return driver
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def project_data_dir() -> pathlib.Path:
|
||||
"""
|
||||
Returns:
|
||||
A path to an OS-specific directory for bumble data. The directory is created if
|
||||
it doesn't exist.
|
||||
"""
|
||||
import platformdirs
|
||||
|
||||
if platform.system() == 'Darwin':
|
||||
# platformdirs doesn't handle macOS right: it doesn't assemble a bundle id
|
||||
# out of author & project
|
||||
return platformdirs.user_data_path(
|
||||
appname='com.google.bumble', ensure_exists=True
|
||||
)
|
||||
else:
|
||||
# windows and linux don't use the com qualifier
|
||||
return platformdirs.user_data_path(
|
||||
appname='bumble', appauthor='google', ensure_exists=True
|
||||
)
|
||||
659
bumble/drivers/rtk.py
Normal file
659
bumble/drivers/rtk.py
Normal file
@@ -0,0 +1,659 @@
|
||||
# Copyright 2021-2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Support for Realtek USB dongles.
|
||||
Based on various online bits of information, including the Linux kernel.
|
||||
(see `drivers/bluetooth/btrtl.c`)
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from dataclasses import dataclass
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import struct
|
||||
from typing import Tuple
|
||||
import weakref
|
||||
|
||||
|
||||
from bumble.hci import (
|
||||
hci_vendor_command_op_code,
|
||||
STATUS_SPEC,
|
||||
HCI_SUCCESS,
|
||||
HCI_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
RTK_ROM_LMP_8723A = 0x1200
|
||||
RTK_ROM_LMP_8723B = 0x8723
|
||||
RTK_ROM_LMP_8821A = 0x8821
|
||||
RTK_ROM_LMP_8761A = 0x8761
|
||||
RTK_ROM_LMP_8822B = 0x8822
|
||||
RTK_ROM_LMP_8852A = 0x8852
|
||||
RTK_CONFIG_MAGIC = 0x8723AB55
|
||||
|
||||
RTK_EPATCH_SIGNATURE = b"Realtech"
|
||||
|
||||
RTK_FRAGMENT_LENGTH = 252
|
||||
|
||||
RTK_FIRMWARE_DIR_ENV = "BUMBLE_RTK_FIRMWARE_DIR"
|
||||
RTK_LINUX_FIRMWARE_DIR = "/lib/firmware/rtl_bt"
|
||||
|
||||
|
||||
class RtlProjectId(enum.IntEnum):
|
||||
PROJECT_ID_8723A = 0
|
||||
PROJECT_ID_8723B = 1
|
||||
PROJECT_ID_8821A = 2
|
||||
PROJECT_ID_8761A = 3
|
||||
PROJECT_ID_8822B = 8
|
||||
PROJECT_ID_8723D = 9
|
||||
PROJECT_ID_8821C = 10
|
||||
PROJECT_ID_8822C = 13
|
||||
PROJECT_ID_8761B = 14
|
||||
PROJECT_ID_8852A = 18
|
||||
PROJECT_ID_8852B = 20
|
||||
PROJECT_ID_8852C = 25
|
||||
|
||||
|
||||
RTK_PROJECT_ID_TO_ROM = {
|
||||
0: RTK_ROM_LMP_8723A,
|
||||
1: RTK_ROM_LMP_8723B,
|
||||
2: RTK_ROM_LMP_8821A,
|
||||
3: RTK_ROM_LMP_8761A,
|
||||
8: RTK_ROM_LMP_8822B,
|
||||
9: RTK_ROM_LMP_8723B,
|
||||
10: RTK_ROM_LMP_8821A,
|
||||
13: RTK_ROM_LMP_8822B,
|
||||
14: RTK_ROM_LMP_8761A,
|
||||
18: RTK_ROM_LMP_8852A,
|
||||
20: RTK_ROM_LMP_8852A,
|
||||
25: RTK_ROM_LMP_8852A,
|
||||
}
|
||||
|
||||
# List of USB (VendorID, ProductID) for Realtek-based devices.
|
||||
RTK_USB_PRODUCTS = {
|
||||
# Realtek 8723AE
|
||||
(0x0930, 0x021D),
|
||||
(0x13D3, 0x3394),
|
||||
# Realtek 8723BE
|
||||
(0x0489, 0xE085),
|
||||
(0x0489, 0xE08B),
|
||||
(0x04F2, 0xB49F),
|
||||
(0x13D3, 0x3410),
|
||||
(0x13D3, 0x3416),
|
||||
(0x13D3, 0x3459),
|
||||
(0x13D3, 0x3494),
|
||||
# Realtek 8723BU
|
||||
(0x7392, 0xA611),
|
||||
# Realtek 8723DE
|
||||
(0x0BDA, 0xB009),
|
||||
(0x2FF8, 0xB011),
|
||||
# Realtek 8761BUV
|
||||
(0x0B05, 0x190E),
|
||||
(0x0BDA, 0x8771),
|
||||
(0x2230, 0x0016),
|
||||
(0x2357, 0x0604),
|
||||
(0x2550, 0x8761),
|
||||
(0x2B89, 0x8761),
|
||||
(0x7392, 0xC611),
|
||||
(0x0BDA, 0x877B),
|
||||
# Realtek 8821AE
|
||||
(0x0B05, 0x17DC),
|
||||
(0x13D3, 0x3414),
|
||||
(0x13D3, 0x3458),
|
||||
(0x13D3, 0x3461),
|
||||
(0x13D3, 0x3462),
|
||||
# Realtek 8821CE
|
||||
(0x0BDA, 0xB00C),
|
||||
(0x0BDA, 0xC822),
|
||||
(0x13D3, 0x3529),
|
||||
# Realtek 8822BE
|
||||
(0x0B05, 0x185C),
|
||||
(0x13D3, 0x3526),
|
||||
# Realtek 8822CE
|
||||
(0x04C5, 0x161F),
|
||||
(0x04CA, 0x4005),
|
||||
(0x0B05, 0x18EF),
|
||||
(0x0BDA, 0xB00C),
|
||||
(0x0BDA, 0xC123),
|
||||
(0x0BDA, 0xC822),
|
||||
(0x0CB5, 0xC547),
|
||||
(0x1358, 0xC123),
|
||||
(0x13D3, 0x3548),
|
||||
(0x13D3, 0x3549),
|
||||
(0x13D3, 0x3553),
|
||||
(0x13D3, 0x3555),
|
||||
(0x2FF8, 0x3051),
|
||||
# Realtek 8822CU
|
||||
(0x13D3, 0x3549),
|
||||
# Realtek 8852AE
|
||||
(0x04C5, 0x165C),
|
||||
(0x04CA, 0x4006),
|
||||
(0x0BDA, 0x2852),
|
||||
(0x0BDA, 0x385A),
|
||||
(0x0BDA, 0x4852),
|
||||
(0x0BDA, 0xC852),
|
||||
(0x0CB8, 0xC549),
|
||||
# Realtek 8852BE
|
||||
(0x0BDA, 0x887B),
|
||||
(0x0CB8, 0xC559),
|
||||
(0x13D3, 0x3571),
|
||||
# Realtek 8852CE
|
||||
(0x04C5, 0x1675),
|
||||
(0x04CA, 0x4007),
|
||||
(0x0CB8, 0xC558),
|
||||
(0x13D3, 0x3586),
|
||||
(0x13D3, 0x3587),
|
||||
(0x13D3, 0x3592),
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# HCI Commands
|
||||
# -----------------------------------------------------------------------------
|
||||
HCI_RTK_READ_ROM_VERSION_COMMAND = hci_vendor_command_op_code(0x6D)
|
||||
HCI_RTK_DOWNLOAD_COMMAND = hci_vendor_command_op_code(0x20)
|
||||
HCI_RTK_DROP_FIRMWARE_COMMAND = hci_vendor_command_op_code(0x66)
|
||||
HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
@HCI_Command.command(return_parameters_fields=[("status", STATUS_SPEC), ("version", 1)])
|
||||
class HCI_RTK_Read_ROM_Version_Command(HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@HCI_Command.command(
|
||||
fields=[("index", 1), ("payload", RTK_FRAGMENT_LENGTH)],
|
||||
return_parameters_fields=[("status", STATUS_SPEC), ("index", 1)],
|
||||
)
|
||||
class HCI_RTK_Download_Command(HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
@HCI_Command.command()
|
||||
class HCI_RTK_Drop_Firmware_Command(HCI_Command):
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Firmware:
|
||||
def __init__(self, firmware):
|
||||
extension_sig = bytes([0x51, 0x04, 0xFD, 0x77])
|
||||
|
||||
if not firmware.startswith(RTK_EPATCH_SIGNATURE):
|
||||
raise ValueError("Firmware does not start with epatch signature")
|
||||
|
||||
if not firmware.endswith(extension_sig):
|
||||
raise ValueError("Firmware does not end with extension sig")
|
||||
|
||||
# The firmware should start with a 14 byte header.
|
||||
epatch_header_size = 14
|
||||
if len(firmware) < epatch_header_size:
|
||||
raise ValueError("Firmware too short")
|
||||
|
||||
# Look for the "project ID", starting from the end.
|
||||
offset = len(firmware) - len(extension_sig)
|
||||
project_id = -1
|
||||
while offset >= epatch_header_size:
|
||||
length, opcode = firmware[offset - 2 : offset]
|
||||
offset -= 2
|
||||
|
||||
if opcode == 0xFF:
|
||||
# End
|
||||
break
|
||||
|
||||
if length == 0:
|
||||
raise ValueError("Invalid 0-length instruction")
|
||||
|
||||
if opcode == 0 and length == 1:
|
||||
project_id = firmware[offset - 1]
|
||||
break
|
||||
|
||||
offset -= length
|
||||
|
||||
if project_id < 0:
|
||||
raise ValueError("Project ID not found")
|
||||
|
||||
self.project_id = project_id
|
||||
|
||||
# Read the patch tables info.
|
||||
self.version, num_patches = struct.unpack("<IH", firmware[8:14])
|
||||
self.patches = []
|
||||
|
||||
# The patches tables are laid out as:
|
||||
# <ChipID_1><ChipID_2>...<ChipID_N> (16 bits each)
|
||||
# <PatchLength_1><PatchLength_2>...<PatchLength_N> (16 bits each)
|
||||
# <PatchOffset_1><PatchOffset_2>...<PatchOffset_N> (32 bits each)
|
||||
if epatch_header_size + 8 * num_patches > len(firmware):
|
||||
raise ValueError("Firmware too short")
|
||||
chip_id_table_offset = epatch_header_size
|
||||
patch_length_table_offset = chip_id_table_offset + 2 * num_patches
|
||||
patch_offset_table_offset = chip_id_table_offset + 4 * num_patches
|
||||
for patch_index in range(num_patches):
|
||||
chip_id_offset = chip_id_table_offset + 2 * patch_index
|
||||
(chip_id,) = struct.unpack_from("<H", firmware, chip_id_offset)
|
||||
(patch_length,) = struct.unpack_from(
|
||||
"<H", firmware, patch_length_table_offset + 2 * patch_index
|
||||
)
|
||||
(patch_offset,) = struct.unpack_from(
|
||||
"<I", firmware, patch_offset_table_offset + 4 * patch_index
|
||||
)
|
||||
if patch_offset + patch_length > len(firmware):
|
||||
raise ValueError("Firmware too short")
|
||||
|
||||
# Get the SVN version for the patch
|
||||
(svn_version,) = struct.unpack_from(
|
||||
"<I", firmware, patch_offset + patch_length - 8
|
||||
)
|
||||
|
||||
# Create a payload with the patch, replacing the last 4 bytes with
|
||||
# the firmware version.
|
||||
self.patches.append(
|
||||
(
|
||||
chip_id,
|
||||
firmware[patch_offset : patch_offset + patch_length - 4]
|
||||
+ struct.pack("<I", self.version),
|
||||
svn_version,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Driver:
|
||||
@dataclass
|
||||
class DriverInfo:
|
||||
rom: int
|
||||
hci: Tuple[int, int]
|
||||
config_needed: bool
|
||||
has_rom_version: bool
|
||||
has_msft_ext: bool = False
|
||||
fw_name: str = ""
|
||||
config_name: str = ""
|
||||
|
||||
DRIVER_INFOS = [
|
||||
# 8723A
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8723A,
|
||||
hci=(0x0B, 0x06),
|
||||
config_needed=False,
|
||||
has_rom_version=False,
|
||||
fw_name="rtl8723a_fw.bin",
|
||||
config_name="",
|
||||
),
|
||||
# 8723B
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8723B,
|
||||
hci=(0x0B, 0x06),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8723b_fw.bin",
|
||||
config_name="rtl8723b_config.bin",
|
||||
),
|
||||
# 8723D
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8723B,
|
||||
hci=(0x0D, 0x08),
|
||||
config_needed=True,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8723d_fw.bin",
|
||||
config_name="rtl8723d_config.bin",
|
||||
),
|
||||
# 8821A
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8821A,
|
||||
hci=(0x0A, 0x06),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8821a_fw.bin",
|
||||
config_name="rtl8821a_config.bin",
|
||||
),
|
||||
# 8821C
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8821A,
|
||||
hci=(0x0C, 0x08),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8821c_fw.bin",
|
||||
config_name="rtl8821c_config.bin",
|
||||
),
|
||||
# 8761A
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8761A,
|
||||
hci=(0x0A, 0x06),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8761a_fw.bin",
|
||||
config_name="rtl8761a_config.bin",
|
||||
),
|
||||
# 8761BU
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8761A,
|
||||
hci=(0x0B, 0x0A),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
fw_name="rtl8761bu_fw.bin",
|
||||
config_name="rtl8761bu_config.bin",
|
||||
),
|
||||
# 8822C
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8822B,
|
||||
hci=(0x0C, 0x0A),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8822cu_fw.bin",
|
||||
config_name="rtl8822cu_config.bin",
|
||||
),
|
||||
# 8822B
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8822B,
|
||||
hci=(0x0B, 0x07),
|
||||
config_needed=True,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8822b_fw.bin",
|
||||
config_name="rtl8822b_config.bin",
|
||||
),
|
||||
# 8852A
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8852A,
|
||||
hci=(0x0A, 0x0B),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8852au_fw.bin",
|
||||
config_name="rtl8852au_config.bin",
|
||||
),
|
||||
# 8852B
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8852A,
|
||||
hci=(0xB, 0xB),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8852bu_fw.bin",
|
||||
config_name="rtl8852bu_config.bin",
|
||||
),
|
||||
# 8852C
|
||||
DriverInfo(
|
||||
rom=RTK_ROM_LMP_8852A,
|
||||
hci=(0x0C, 0x0C),
|
||||
config_needed=False,
|
||||
has_rom_version=True,
|
||||
has_msft_ext=True,
|
||||
fw_name="rtl8852cu_fw.bin",
|
||||
config_name="rtl8852cu_config.bin",
|
||||
),
|
||||
]
|
||||
|
||||
POST_DROP_DELAY = 0.2
|
||||
|
||||
@staticmethod
|
||||
def find_driver_info(hci_version, hci_subversion, lmp_subversion):
|
||||
for driver_info in Driver.DRIVER_INFOS:
|
||||
if driver_info.rom == lmp_subversion and driver_info.hci == (
|
||||
hci_subversion,
|
||||
hci_version,
|
||||
):
|
||||
return driver_info
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_binary_path(file_name):
|
||||
# First check if an environment variable is set
|
||||
if RTK_FIRMWARE_DIR_ENV in os.environ:
|
||||
if (
|
||||
path := pathlib.Path(os.environ[RTK_FIRMWARE_DIR_ENV]) / file_name
|
||||
).is_file():
|
||||
logger.debug(f"{file_name} found in env dir")
|
||||
return path
|
||||
|
||||
# When the environment variable is set, don't look elsewhere
|
||||
return None
|
||||
|
||||
# Then, look where the firmware download tool writes by default
|
||||
if (path := rtk_firmware_dir() / file_name).is_file():
|
||||
logger.debug(f"{file_name} found in project data dir")
|
||||
return path
|
||||
|
||||
# Then, look in the package's driver directory
|
||||
if (path := pathlib.Path(__file__).parent / "rtk_fw" / file_name).is_file():
|
||||
logger.debug(f"{file_name} found in package dir")
|
||||
return path
|
||||
|
||||
# On Linux, check the system's FW directory
|
||||
if (
|
||||
platform.system() == "Linux"
|
||||
and (path := pathlib.Path(RTK_LINUX_FIRMWARE_DIR) / file_name).is_file()
|
||||
):
|
||||
logger.debug(f"{file_name} found in Linux system FW dir")
|
||||
return path
|
||||
|
||||
# Finally look in the current directory
|
||||
if (path := pathlib.Path.cwd() / file_name).is_file():
|
||||
logger.debug(f"{file_name} found in CWD")
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def check(host):
|
||||
if not host.hci_metadata:
|
||||
logger.debug("USB metadata not found")
|
||||
return False
|
||||
|
||||
vendor_id = host.hci_metadata.get("vendor_id", None)
|
||||
product_id = host.hci_metadata.get("product_id", None)
|
||||
if vendor_id is None or product_id is None:
|
||||
logger.debug("USB metadata not sufficient")
|
||||
return False
|
||||
|
||||
if (vendor_id, product_id) not in RTK_USB_PRODUCTS:
|
||||
logger.debug(
|
||||
f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def driver_info_for_host(cls, host):
|
||||
response = await host.send_command(
|
||||
HCI_Read_Local_Version_Information_Command(), check_result=True
|
||||
)
|
||||
local_version = response.return_parameters
|
||||
|
||||
logger.debug(
|
||||
f"looking for a driver: 0x{local_version.lmp_subversion:04X} "
|
||||
f"(0x{local_version.hci_version:02X}, "
|
||||
f"0x{local_version.hci_subversion:04X})"
|
||||
)
|
||||
|
||||
driver_info = cls.find_driver_info(
|
||||
local_version.hci_version,
|
||||
local_version.hci_subversion,
|
||||
local_version.lmp_subversion,
|
||||
)
|
||||
if driver_info is None:
|
||||
# TODO: it seems that the Linux driver will send command (0x3f, 0x66)
|
||||
# in this case and then re-read the local version, then re-match.
|
||||
logger.debug("firmware already loaded or no known driver for this device")
|
||||
|
||||
return driver_info
|
||||
|
||||
@classmethod
|
||||
async def for_host(cls, host, force=False):
|
||||
# Check that a driver is needed for this host
|
||||
if not force and not cls.check(host):
|
||||
return None
|
||||
|
||||
# Get the driver info
|
||||
driver_info = await cls.driver_info_for_host(host)
|
||||
if driver_info is None:
|
||||
return None
|
||||
|
||||
# Load the firmware
|
||||
firmware_path = cls.find_binary_path(driver_info.fw_name)
|
||||
if not firmware_path:
|
||||
logger.warning(f"Firmware file {driver_info.fw_name} not found")
|
||||
logger.warning("See https://google.github.io/bumble/drivers/realtek.html")
|
||||
return None
|
||||
with open(firmware_path, "rb") as firmware_file:
|
||||
firmware = firmware_file.read()
|
||||
|
||||
# Load the config
|
||||
config = None
|
||||
if driver_info.config_name:
|
||||
config_path = cls.find_binary_path(driver_info.config_name)
|
||||
if config_path:
|
||||
with open(config_path, "rb") as config_file:
|
||||
config = config_file.read()
|
||||
if driver_info.config_needed and not config:
|
||||
logger.warning("Config needed, but no config file available")
|
||||
return None
|
||||
|
||||
return cls(host, driver_info, firmware, config)
|
||||
|
||||
def __init__(self, host, driver_info, firmware, config):
|
||||
self.host = weakref.proxy(host)
|
||||
self.driver_info = driver_info
|
||||
self.firmware = firmware
|
||||
self.config = config
|
||||
|
||||
@staticmethod
|
||||
async def drop_firmware(host):
|
||||
host.send_hci_packet(HCI_RTK_Drop_Firmware_Command())
|
||||
|
||||
# Wait for the command to be effective (no response is sent)
|
||||
await asyncio.sleep(Driver.POST_DROP_DELAY)
|
||||
|
||||
async def download_for_rtl8723a(self):
|
||||
# Check that the firmware image does not include an epatch signature.
|
||||
if RTK_EPATCH_SIGNATURE in self.firmware:
|
||||
logger.warning(
|
||||
"epatch signature found in firmware, it is probably the wrong firmware"
|
||||
)
|
||||
return
|
||||
|
||||
# TODO: load the firmware
|
||||
|
||||
async def download_for_rtl8723b(self):
|
||||
if self.driver_info.has_rom_version:
|
||||
response = await self.host.send_command(
|
||||
HCI_RTK_Read_ROM_Version_Command(), check_result=True
|
||||
)
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
return
|
||||
rom_version = response.return_parameters.version
|
||||
logger.debug(f"ROM version before download: {rom_version:04X}")
|
||||
else:
|
||||
rom_version = 0
|
||||
|
||||
firmware = Firmware(self.firmware)
|
||||
logger.debug(f"firmware: project_id=0x{firmware.project_id:04X}")
|
||||
for patch in firmware.patches:
|
||||
if patch[0] == rom_version + 1:
|
||||
logger.debug(f"using patch {patch[0]}")
|
||||
break
|
||||
else:
|
||||
logger.warning("no valid patch found for rom version {rom_version}")
|
||||
return
|
||||
|
||||
# Append the config if there is one.
|
||||
if self.config:
|
||||
payload = patch[1] + self.config
|
||||
else:
|
||||
payload = patch[1]
|
||||
|
||||
# Download the payload, one fragment at a time.
|
||||
fragment_count = math.ceil(len(payload) / RTK_FRAGMENT_LENGTH)
|
||||
for fragment_index in range(fragment_count):
|
||||
# NOTE: the Linux driver somehow adds 1 to the index after it wraps around.
|
||||
# That's odd, but we"ll do the same here.
|
||||
download_index = fragment_index & 0x7F
|
||||
if download_index >= 0x80:
|
||||
download_index += 1
|
||||
if fragment_index == fragment_count - 1:
|
||||
download_index |= 0x80 # End marker.
|
||||
fragment_offset = fragment_index * RTK_FRAGMENT_LENGTH
|
||||
fragment = payload[fragment_offset : fragment_offset + RTK_FRAGMENT_LENGTH]
|
||||
logger.debug(f"downloading fragment {fragment_index}")
|
||||
await self.host.send_command(
|
||||
HCI_RTK_Download_Command(
|
||||
index=download_index, payload=fragment, check_result=True
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug("download complete!")
|
||||
|
||||
# Read the version again
|
||||
response = await self.host.send_command(
|
||||
HCI_RTK_Read_ROM_Version_Command(), check_result=True
|
||||
)
|
||||
if response.return_parameters.status != HCI_SUCCESS:
|
||||
logger.warning("can't get ROM version")
|
||||
else:
|
||||
rom_version = response.return_parameters.version
|
||||
logger.debug(f"ROM version after download: {rom_version:04X}")
|
||||
|
||||
async def download_firmware(self):
|
||||
if self.driver_info.rom == RTK_ROM_LMP_8723A:
|
||||
return await self.download_for_rtl8723a()
|
||||
|
||||
if self.driver_info.rom in (
|
||||
RTK_ROM_LMP_8723B,
|
||||
RTK_ROM_LMP_8821A,
|
||||
RTK_ROM_LMP_8761A,
|
||||
RTK_ROM_LMP_8822B,
|
||||
RTK_ROM_LMP_8852A,
|
||||
):
|
||||
return await self.download_for_rtl8723b()
|
||||
|
||||
raise ValueError("ROM not supported")
|
||||
|
||||
async def init_controller(self):
|
||||
await self.download_firmware()
|
||||
await self.host.send_command(HCI_Reset_Command(), check_result=True)
|
||||
logger.info(f"loaded FW image {self.driver_info.fw_name}")
|
||||
|
||||
|
||||
def rtk_firmware_dir() -> pathlib.Path:
|
||||
"""
|
||||
Returns:
|
||||
A path to a subdir of the project data dir for Realtek firmware.
|
||||
The directory is created if it doesn't exist.
|
||||
"""
|
||||
from bumble.drivers import project_data_dir
|
||||
|
||||
p = project_data_dir() / "firmware" / "realtek"
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
@@ -28,7 +28,7 @@ import enum
|
||||
import functools
|
||||
import logging
|
||||
import struct
|
||||
from typing import Optional, Sequence, List
|
||||
from typing import Optional, Sequence, Iterable, List, Union
|
||||
|
||||
from .colors import color
|
||||
from .core import UUID, get_dict_key_by_value
|
||||
@@ -187,7 +187,7 @@ GATT_CENTRAL_ADDRESS_RESOLUTION__CHARACTERISTIC = UUID.from_16_bi
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def show_services(services):
|
||||
def show_services(services: Iterable[Service]) -> None:
|
||||
for service in services:
|
||||
print(color(str(service), 'cyan'))
|
||||
|
||||
@@ -210,11 +210,11 @@ class Service(Attribute):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uuid,
|
||||
uuid: Union[str, UUID],
|
||||
characteristics: List[Characteristic],
|
||||
primary=True,
|
||||
included_services: List[Service] = [],
|
||||
):
|
||||
) -> None:
|
||||
# Convert the uuid to a UUID object if it isn't already
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
@@ -239,7 +239,7 @@ class Service(Attribute):
|
||||
"""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'Service(handle=0x{self.handle:04X}, '
|
||||
f'end=0x{self.end_group_handle:04X}, '
|
||||
@@ -255,9 +255,11 @@ class TemplateService(Service):
|
||||
to expose their UUID as a class property
|
||||
'''
|
||||
|
||||
UUID: Optional[UUID] = None
|
||||
UUID: UUID
|
||||
|
||||
def __init__(self, characteristics, primary=True):
|
||||
def __init__(
|
||||
self, characteristics: List[Characteristic], primary: bool = True
|
||||
) -> None:
|
||||
super().__init__(self.UUID, characteristics, primary)
|
||||
|
||||
|
||||
@@ -269,7 +271,7 @@ class IncludedServiceDeclaration(Attribute):
|
||||
|
||||
service: Service
|
||||
|
||||
def __init__(self, service):
|
||||
def __init__(self, service: Service) -> None:
|
||||
declaration_bytes = struct.pack(
|
||||
'<HH2s', service.handle, service.end_group_handle, service.uuid.to_bytes()
|
||||
)
|
||||
@@ -278,13 +280,12 @@ class IncludedServiceDeclaration(Attribute):
|
||||
)
|
||||
self.service = service
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'IncludedServiceDefinition(handle=0x{self.handle:04X}, '
|
||||
f'group_starting_handle=0x{self.service.handle:04X}, '
|
||||
f'group_ending_handle=0x{self.service.end_group_handle:04X}, '
|
||||
f'uuid={self.service.uuid}, '
|
||||
f'{self.service.properties!s})'
|
||||
f'uuid={self.service.uuid})'
|
||||
)
|
||||
|
||||
|
||||
@@ -309,31 +310,33 @@ class Characteristic(Attribute):
|
||||
AUTHENTICATED_SIGNED_WRITES = 0x40
|
||||
EXTENDED_PROPERTIES = 0x80
|
||||
|
||||
@staticmethod
|
||||
def from_string(properties_str: str) -> Characteristic.Properties:
|
||||
property_names: List[str] = []
|
||||
for property in Characteristic.Properties:
|
||||
if property.name is None:
|
||||
raise TypeError()
|
||||
property_names.append(property.name)
|
||||
|
||||
def string_to_property(property_string) -> Characteristic.Properties:
|
||||
for property in zip(Characteristic.Properties, property_names):
|
||||
if property_string == property[1]:
|
||||
return property[0]
|
||||
raise TypeError(f"Unable to convert {property_string} to Property")
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, properties_str: str) -> Characteristic.Properties:
|
||||
try:
|
||||
return functools.reduce(
|
||||
lambda x, y: x | string_to_property(y),
|
||||
properties_str.split(","),
|
||||
lambda x, y: x | cls[y],
|
||||
properties_str.replace("|", ",").split(","),
|
||||
Characteristic.Properties(0),
|
||||
)
|
||||
except TypeError:
|
||||
except (TypeError, KeyError):
|
||||
# The check for `p.name is not None` here is needed because for InFlag
|
||||
# enums, the .name property can be None, when the enum value is 0,
|
||||
# so the type hint for .name is Optional[str].
|
||||
enum_list: List[str] = [p.name for p in cls if p.name is not None]
|
||||
enum_list_str = ",".join(enum_list)
|
||||
raise TypeError(
|
||||
f"Characteristic.Properties::from_string() error:\nExpected a string containing any of the keys, separated by commas: {','.join(property_names)}\nGot: {properties_str}"
|
||||
f"Characteristic.Properties::from_string() error:\nExpected a string containing any of the keys, separated by , or |: {enum_list_str}\nGot: {properties_str}"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
# NOTE: we override this method to offer a consistent result between python
|
||||
# versions: the value returned by IntFlag.__str__() changed in version 11.
|
||||
return '|'.join(
|
||||
flag.name
|
||||
for flag in Characteristic.Properties
|
||||
if self.value & flag.value and flag.name is not None
|
||||
)
|
||||
|
||||
# For backwards compatibility these are defined here
|
||||
# For new code, please use Characteristic.Properties.X
|
||||
BROADCAST = Properties.BROADCAST
|
||||
@@ -347,10 +350,10 @@ class Characteristic(Attribute):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uuid,
|
||||
uuid: Union[str, bytes, UUID],
|
||||
properties: Characteristic.Properties,
|
||||
permissions,
|
||||
value=b'',
|
||||
permissions: Union[str, Attribute.Permissions],
|
||||
value: Union[str, bytes, CharacteristicValue] = b'',
|
||||
descriptors: Sequence[Descriptor] = (),
|
||||
):
|
||||
super().__init__(uuid, permissions, value)
|
||||
@@ -368,12 +371,12 @@ class Characteristic(Attribute):
|
||||
def has_properties(self, properties: Characteristic.Properties) -> bool:
|
||||
return self.properties & properties == properties
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'Characteristic(handle=0x{self.handle:04X}, '
|
||||
f'end=0x{self.end_group_handle:04X}, '
|
||||
f'uuid={self.uuid}, '
|
||||
f'{self.properties!s})'
|
||||
f'{self.properties})'
|
||||
)
|
||||
|
||||
|
||||
@@ -385,7 +388,7 @@ class CharacteristicDeclaration(Attribute):
|
||||
|
||||
characteristic: Characteristic
|
||||
|
||||
def __init__(self, characteristic, value_handle):
|
||||
def __init__(self, characteristic: Characteristic, value_handle: int) -> None:
|
||||
declaration_bytes = (
|
||||
struct.pack('<BH', characteristic.properties, value_handle)
|
||||
+ characteristic.uuid.to_pdu_bytes()
|
||||
@@ -396,12 +399,12 @@ class CharacteristicDeclaration(Attribute):
|
||||
self.value_handle = value_handle
|
||||
self.characteristic = characteristic
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'CharacteristicDeclaration(handle=0x{self.handle:04X}, '
|
||||
f'value_handle=0x{self.value_handle:04X}, '
|
||||
f'uuid={self.characteristic.uuid}, '
|
||||
f'{self.characteristic.properties!s})'
|
||||
f'{self.characteristic.properties})'
|
||||
)
|
||||
|
||||
|
||||
@@ -519,7 +522,7 @@ class CharacteristicAdapter:
|
||||
|
||||
return self.wrapped_characteristic.unsubscribe(subscriber)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
wrapped = str(self.wrapped_characteristic)
|
||||
return f'{self.__class__.__name__}({wrapped})'
|
||||
|
||||
@@ -599,10 +602,10 @@ class UTF8CharacteristicAdapter(CharacteristicAdapter):
|
||||
Adapter that converts strings to/from bytes using UTF-8 encoding
|
||||
'''
|
||||
|
||||
def encode_value(self, value):
|
||||
def encode_value(self, value: str) -> bytes:
|
||||
return value.encode('utf-8')
|
||||
|
||||
def decode_value(self, value):
|
||||
def decode_value(self, value: bytes) -> str:
|
||||
return value.decode('utf-8')
|
||||
|
||||
|
||||
@@ -612,7 +615,7 @@ class Descriptor(Attribute):
|
||||
See Vol 3, Part G - 3.3.3 Characteristic Descriptor Declarations
|
||||
'''
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'Descriptor(handle=0x{self.handle:04X}, '
|
||||
f'type={self.type}, '
|
||||
|
||||
@@ -28,7 +28,18 @@ import asyncio
|
||||
import logging
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Tuple, Callable, Union, Any
|
||||
from typing import (
|
||||
List,
|
||||
Optional,
|
||||
Dict,
|
||||
Tuple,
|
||||
Callable,
|
||||
Union,
|
||||
Any,
|
||||
Iterable,
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
@@ -66,8 +77,12 @@ from .gatt import (
|
||||
GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||
Characteristic,
|
||||
ClientCharacteristicConfigurationBits,
|
||||
TemplateService,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Connection
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -78,16 +93,16 @@ logger = logging.getLogger(__name__)
|
||||
# Proxies
|
||||
# -----------------------------------------------------------------------------
|
||||
class AttributeProxy(EventEmitter):
|
||||
client: Client
|
||||
|
||||
def __init__(self, client, handle, end_group_handle, attribute_type):
|
||||
def __init__(
|
||||
self, client: Client, handle: int, end_group_handle: int, attribute_type: UUID
|
||||
) -> None:
|
||||
EventEmitter.__init__(self)
|
||||
self.client = client
|
||||
self.handle = handle
|
||||
self.end_group_handle = end_group_handle
|
||||
self.type = attribute_type
|
||||
|
||||
async def read_value(self, no_long_read=False):
|
||||
async def read_value(self, no_long_read: bool = False) -> bytes:
|
||||
return self.decode_value(
|
||||
await self.client.read_value(self.handle, no_long_read)
|
||||
)
|
||||
@@ -97,13 +112,13 @@ class AttributeProxy(EventEmitter):
|
||||
self.handle, self.encode_value(value), with_response
|
||||
)
|
||||
|
||||
def encode_value(self, value):
|
||||
def encode_value(self, value: Any) -> bytes:
|
||||
return value
|
||||
|
||||
def decode_value(self, value_bytes):
|
||||
def decode_value(self, value_bytes: bytes) -> Any:
|
||||
return value_bytes
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f'Attribute(handle=0x{self.handle:04X}, type={self.type})'
|
||||
|
||||
|
||||
@@ -136,14 +151,14 @@ class ServiceProxy(AttributeProxy):
|
||||
def get_characteristics_by_uuid(self, uuid):
|
||||
return self.client.get_characteristics_by_uuid(uuid, self)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f'Service(handle=0x{self.handle:04X}, uuid={self.uuid})'
|
||||
|
||||
|
||||
class CharacteristicProxy(AttributeProxy):
|
||||
properties: Characteristic.Properties
|
||||
descriptors: List[DescriptorProxy]
|
||||
subscribers: Dict[Any, Callable]
|
||||
subscribers: Dict[Any, Callable[[bytes], Any]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -171,7 +186,9 @@ class CharacteristicProxy(AttributeProxy):
|
||||
return await self.client.discover_descriptors(self)
|
||||
|
||||
async def subscribe(
|
||||
self, subscriber: Optional[Callable] = None, prefer_notify=True
|
||||
self,
|
||||
subscriber: Optional[Callable[[bytes], Any]] = None,
|
||||
prefer_notify: bool = True,
|
||||
):
|
||||
if subscriber is not None:
|
||||
if subscriber in self.subscribers:
|
||||
@@ -195,7 +212,7 @@ class CharacteristicProxy(AttributeProxy):
|
||||
|
||||
return await self.client.unsubscribe(self, subscriber)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'Characteristic(handle=0x{self.handle:04X}, '
|
||||
f'uuid={self.uuid}, '
|
||||
@@ -207,7 +224,7 @@ class DescriptorProxy(AttributeProxy):
|
||||
def __init__(self, client, handle, descriptor_type):
|
||||
super().__init__(client, handle, 0, descriptor_type)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f'Descriptor(handle=0x{self.handle:04X}, type={self.type})'
|
||||
|
||||
|
||||
@@ -216,8 +233,10 @@ class ProfileServiceProxy:
|
||||
Base class for profile-specific service proxies
|
||||
'''
|
||||
|
||||
SERVICE_CLASS: Type[TemplateService]
|
||||
|
||||
@classmethod
|
||||
def from_client(cls, client):
|
||||
def from_client(cls, client: Client) -> ProfileServiceProxy:
|
||||
return ServiceProxy.from_client(cls, client, cls.SERVICE_CLASS.UUID)
|
||||
|
||||
|
||||
@@ -227,8 +246,12 @@ class ProfileServiceProxy:
|
||||
class Client:
|
||||
services: List[ServiceProxy]
|
||||
cached_values: Dict[int, Tuple[datetime, bytes]]
|
||||
notification_subscribers: Dict[int, Callable[[bytes], Any]]
|
||||
indication_subscribers: Dict[int, Callable[[bytes], Any]]
|
||||
pending_response: Optional[asyncio.futures.Future[ATT_PDU]]
|
||||
pending_request: Optional[ATT_PDU]
|
||||
|
||||
def __init__(self, connection):
|
||||
def __init__(self, connection: Connection) -> None:
|
||||
self.connection = connection
|
||||
self.mtu_exchange_done = False
|
||||
self.request_semaphore = asyncio.Semaphore(1)
|
||||
@@ -241,16 +264,16 @@ class Client:
|
||||
self.services = []
|
||||
self.cached_values = {}
|
||||
|
||||
def send_gatt_pdu(self, pdu):
|
||||
def send_gatt_pdu(self, pdu: bytes) -> None:
|
||||
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
||||
|
||||
async def send_command(self, command):
|
||||
async def send_command(self, command: ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Command from client: [0x{self.connection.handle:04X}] {command}'
|
||||
)
|
||||
self.send_gatt_pdu(command.to_bytes())
|
||||
|
||||
async def send_request(self, request):
|
||||
async def send_request(self, request: ATT_PDU):
|
||||
logger.debug(
|
||||
f'GATT Request from client: [0x{self.connection.handle:04X}] {request}'
|
||||
)
|
||||
@@ -279,14 +302,14 @@ class Client:
|
||||
|
||||
return response
|
||||
|
||||
def send_confirmation(self, confirmation):
|
||||
def send_confirmation(self, confirmation: ATT_Handle_Value_Confirmation) -> None:
|
||||
logger.debug(
|
||||
f'GATT Confirmation from client: [0x{self.connection.handle:04X}] '
|
||||
f'{confirmation}'
|
||||
)
|
||||
self.send_gatt_pdu(confirmation.to_bytes())
|
||||
|
||||
async def request_mtu(self, mtu):
|
||||
async def request_mtu(self, mtu: int) -> int:
|
||||
# Check the range
|
||||
if mtu < ATT_DEFAULT_MTU:
|
||||
raise ValueError(f'MTU must be >= {ATT_DEFAULT_MTU}')
|
||||
@@ -313,10 +336,12 @@ class Client:
|
||||
|
||||
return self.connection.att_mtu
|
||||
|
||||
def get_services_by_uuid(self, uuid):
|
||||
def get_services_by_uuid(self, uuid: UUID) -> List[ServiceProxy]:
|
||||
return [service for service in self.services if service.uuid == uuid]
|
||||
|
||||
def get_characteristics_by_uuid(self, uuid, service=None):
|
||||
def get_characteristics_by_uuid(
|
||||
self, uuid: UUID, service: Optional[ServiceProxy] = None
|
||||
) -> List[CharacteristicProxy]:
|
||||
services = [service] if service else self.services
|
||||
return [
|
||||
c
|
||||
@@ -363,7 +388,7 @@ class Client:
|
||||
if not already_known:
|
||||
self.services.append(service)
|
||||
|
||||
async def discover_services(self, uuids=None) -> List[ServiceProxy]:
|
||||
async def discover_services(self, uuids: Iterable[UUID] = []) -> List[ServiceProxy]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.4.1 Discover All Primary Services
|
||||
'''
|
||||
@@ -435,7 +460,7 @@ class Client:
|
||||
|
||||
return services
|
||||
|
||||
async def discover_service(self, uuid):
|
||||
async def discover_service(self, uuid: Union[str, UUID]) -> List[ServiceProxy]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID
|
||||
'''
|
||||
@@ -468,7 +493,7 @@ class Client:
|
||||
f'{HCI_Constant.error_name(response.error_code)}'
|
||||
)
|
||||
# TODO raise appropriate exception
|
||||
return
|
||||
return []
|
||||
break
|
||||
|
||||
for attribute_handle, end_group_handle in response.handles_information:
|
||||
@@ -480,7 +505,7 @@ class Client:
|
||||
logger.warning(
|
||||
f'bogus handle values: {attribute_handle} {end_group_handle}'
|
||||
)
|
||||
return
|
||||
return []
|
||||
|
||||
# Create a service proxy for this service
|
||||
service = ServiceProxy(
|
||||
@@ -721,7 +746,7 @@ class Client:
|
||||
|
||||
return descriptors
|
||||
|
||||
async def discover_attributes(self):
|
||||
async def discover_attributes(self) -> List[AttributeProxy]:
|
||||
'''
|
||||
Discover all attributes, regardless of type
|
||||
'''
|
||||
@@ -844,7 +869,9 @@ class Client:
|
||||
# No more subscribers left
|
||||
await self.write_value(cccd, b'\x00\x00', with_response=True)
|
||||
|
||||
async def read_value(self, attribute, no_long_read=False):
|
||||
async def read_value(
|
||||
self, attribute: Union[int, AttributeProxy], no_long_read: bool = False
|
||||
) -> Any:
|
||||
'''
|
||||
See Vol 3, Part G - 4.8.1 Read Characteristic Value
|
||||
|
||||
@@ -905,7 +932,9 @@ class Client:
|
||||
# Return the value as bytes
|
||||
return attribute_value
|
||||
|
||||
async def read_characteristics_by_uuid(self, uuid, service):
|
||||
async def read_characteristics_by_uuid(
|
||||
self, uuid: UUID, service: Optional[ServiceProxy]
|
||||
) -> List[bytes]:
|
||||
'''
|
||||
See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID
|
||||
'''
|
||||
@@ -960,7 +989,12 @@ class Client:
|
||||
|
||||
return characteristics_values
|
||||
|
||||
async def write_value(self, attribute, value, with_response=False):
|
||||
async def write_value(
|
||||
self,
|
||||
attribute: Union[int, AttributeProxy],
|
||||
value: bytes,
|
||||
with_response: bool = False,
|
||||
) -> None:
|
||||
'''
|
||||
See Vol 3, Part G - 4.9.1 Write Without Response & 4.9.3 Write Characteristic
|
||||
Value
|
||||
@@ -990,7 +1024,7 @@ class Client:
|
||||
)
|
||||
)
|
||||
|
||||
def on_gatt_pdu(self, att_pdu):
|
||||
def on_gatt_pdu(self, att_pdu: ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Response to client: [0x{self.connection.handle:04X}] {att_pdu}'
|
||||
)
|
||||
@@ -1013,6 +1047,7 @@ class Client:
|
||||
return
|
||||
|
||||
# Return the response to the coroutine that is waiting for it
|
||||
assert self.pending_response is not None
|
||||
self.pending_response.set_result(att_pdu)
|
||||
else:
|
||||
handler_name = f'on_{att_pdu.name.lower()}'
|
||||
@@ -1060,7 +1095,7 @@ class Client:
|
||||
# Confirm that we received the indication
|
||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||
|
||||
def cache_value(self, attribute_handle: int, value: bytes):
|
||||
def cache_value(self, attribute_handle: int, value: bytes) -> None:
|
||||
self.cached_values[attribute_handle] = (
|
||||
datetime.now(),
|
||||
value,
|
||||
|
||||
@@ -23,11 +23,12 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
import struct
|
||||
from typing import List, Tuple, Optional, TypeVar, Type
|
||||
from typing import List, Tuple, Optional, TypeVar, Type, Dict, Iterable, TYPE_CHECKING
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .colors import color
|
||||
@@ -42,6 +43,7 @@ from .att import (
|
||||
ATT_INVALID_OFFSET_ERROR,
|
||||
ATT_REQUEST_NOT_SUPPORTED_ERROR,
|
||||
ATT_REQUESTS,
|
||||
ATT_PDU,
|
||||
ATT_UNLIKELY_ERROR_ERROR,
|
||||
ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
|
||||
ATT_Error,
|
||||
@@ -73,6 +75,8 @@ from .gatt import (
|
||||
Service,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -91,8 +95,13 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server(EventEmitter):
|
||||
attributes: List[Attribute]
|
||||
services: List[Service]
|
||||
attributes_by_handle: Dict[int, Attribute]
|
||||
subscribers: Dict[int, Dict[int, bytes]]
|
||||
indication_semaphores: defaultdict[int, asyncio.Semaphore]
|
||||
pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, device: Device) -> None:
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.services = []
|
||||
@@ -107,16 +116,16 @@ class Server(EventEmitter):
|
||||
self.indication_semaphores = defaultdict(lambda: asyncio.Semaphore(1))
|
||||
self.pending_confirmations = defaultdict(lambda: None)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return "\n".join(map(str, self.attributes))
|
||||
|
||||
def send_gatt_pdu(self, connection_handle, pdu):
|
||||
def send_gatt_pdu(self, connection_handle: int, pdu: bytes) -> None:
|
||||
self.device.send_l2cap_pdu(connection_handle, ATT_CID, pdu)
|
||||
|
||||
def next_handle(self):
|
||||
def next_handle(self) -> int:
|
||||
return 1 + len(self.attributes)
|
||||
|
||||
def get_advertising_service_data(self):
|
||||
def get_advertising_service_data(self) -> Dict[Attribute, bytes]:
|
||||
return {
|
||||
attribute: data
|
||||
for attribute in self.attributes
|
||||
@@ -124,7 +133,7 @@ class Server(EventEmitter):
|
||||
and (data := attribute.get_advertising_data())
|
||||
}
|
||||
|
||||
def get_attribute(self, handle):
|
||||
def get_attribute(self, handle: int) -> Optional[Attribute]:
|
||||
attribute = self.attributes_by_handle.get(handle)
|
||||
if attribute:
|
||||
return attribute
|
||||
@@ -173,12 +182,17 @@ class Server(EventEmitter):
|
||||
|
||||
return next(
|
||||
(
|
||||
(attribute, self.get_attribute(attribute.characteristic.handle))
|
||||
(
|
||||
attribute,
|
||||
self.get_attribute(attribute.characteristic.handle),
|
||||
) # type: ignore
|
||||
for attribute in map(
|
||||
self.get_attribute,
|
||||
range(service_handle.handle, service_handle.end_group_handle + 1),
|
||||
)
|
||||
if attribute.type == GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||
if attribute is not None
|
||||
and attribute.type == GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||
and isinstance(attribute, CharacteristicDeclaration)
|
||||
and attribute.characteristic.uuid == characteristic_uuid
|
||||
),
|
||||
None,
|
||||
@@ -197,7 +211,7 @@ class Server(EventEmitter):
|
||||
|
||||
return next(
|
||||
(
|
||||
attribute
|
||||
attribute # type: ignore
|
||||
for attribute in map(
|
||||
self.get_attribute,
|
||||
range(
|
||||
@@ -205,12 +219,12 @@ class Server(EventEmitter):
|
||||
characteristic_value.end_group_handle + 1,
|
||||
),
|
||||
)
|
||||
if attribute.type == descriptor_uuid
|
||||
if attribute is not None and attribute.type == descriptor_uuid
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
def add_attribute(self, attribute):
|
||||
def add_attribute(self, attribute: Attribute) -> None:
|
||||
# Assign a handle to this attribute
|
||||
attribute.handle = self.next_handle()
|
||||
attribute.end_group_handle = (
|
||||
@@ -220,7 +234,7 @@ class Server(EventEmitter):
|
||||
# Add this attribute to the list
|
||||
self.attributes.append(attribute)
|
||||
|
||||
def add_service(self, service: Service):
|
||||
def add_service(self, service: Service) -> None:
|
||||
# Add the service attribute to the DB
|
||||
self.add_attribute(service)
|
||||
|
||||
@@ -285,11 +299,13 @@ class Server(EventEmitter):
|
||||
service.end_group_handle = self.attributes[-1].handle
|
||||
self.services.append(service)
|
||||
|
||||
def add_services(self, services):
|
||||
def add_services(self, services: Iterable[Service]) -> None:
|
||||
for service in services:
|
||||
self.add_service(service)
|
||||
|
||||
def read_cccd(self, connection, characteristic):
|
||||
def read_cccd(
|
||||
self, connection: Optional[Connection], characteristic: Characteristic
|
||||
) -> bytes:
|
||||
if connection is None:
|
||||
return bytes([0, 0])
|
||||
|
||||
@@ -300,7 +316,12 @@ class Server(EventEmitter):
|
||||
|
||||
return cccd or bytes([0, 0])
|
||||
|
||||
def write_cccd(self, connection, characteristic, value):
|
||||
def write_cccd(
|
||||
self,
|
||||
connection: Connection,
|
||||
characteristic: Characteristic,
|
||||
value: bytes,
|
||||
) -> None:
|
||||
logger.debug(
|
||||
f'Subscription update for connection=0x{connection.handle:04X}, '
|
||||
f'handle=0x{characteristic.handle:04X}: {value.hex()}'
|
||||
@@ -327,13 +348,19 @@ class Server(EventEmitter):
|
||||
indicate_enabled,
|
||||
)
|
||||
|
||||
def send_response(self, connection, response):
|
||||
def send_response(self, connection: Connection, response: ATT_PDU) -> None:
|
||||
logger.debug(
|
||||
f'GATT Response from server: [0x{connection.handle:04X}] {response}'
|
||||
)
|
||||
self.send_gatt_pdu(connection.handle, response.to_bytes())
|
||||
|
||||
async def notify_subscriber(self, connection, attribute, value=None, force=False):
|
||||
async def notify_subscriber(
|
||||
self,
|
||||
connection: Connection,
|
||||
attribute: Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
# Check if there's a subscriber
|
||||
if not force:
|
||||
subscribers = self.subscribers.get(connection.handle)
|
||||
@@ -370,7 +397,13 @@ class Server(EventEmitter):
|
||||
)
|
||||
self.send_gatt_pdu(connection.handle, bytes(notification))
|
||||
|
||||
async def indicate_subscriber(self, connection, attribute, value=None, force=False):
|
||||
async def indicate_subscriber(
|
||||
self,
|
||||
connection: Connection,
|
||||
attribute: Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
# Check if there's a subscriber
|
||||
if not force:
|
||||
subscribers = self.subscribers.get(connection.handle)
|
||||
@@ -411,15 +444,13 @@ class Server(EventEmitter):
|
||||
assert self.pending_confirmations[connection.handle] is None
|
||||
|
||||
# Create a future value to hold the eventual response
|
||||
self.pending_confirmations[
|
||||
pending_confirmation = self.pending_confirmations[
|
||||
connection.handle
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
|
||||
try:
|
||||
self.send_gatt_pdu(connection.handle, indication.to_bytes())
|
||||
await asyncio.wait_for(
|
||||
self.pending_confirmations[connection.handle], GATT_REQUEST_TIMEOUT
|
||||
)
|
||||
await asyncio.wait_for(pending_confirmation, GATT_REQUEST_TIMEOUT)
|
||||
except asyncio.TimeoutError as error:
|
||||
logger.warning(color('!!! GATT Indicate timeout', 'red'))
|
||||
raise TimeoutError(f'GATT timeout for {indication.name}') from error
|
||||
@@ -427,8 +458,12 @@ class Server(EventEmitter):
|
||||
self.pending_confirmations[connection.handle] = None
|
||||
|
||||
async def notify_or_indicate_subscribers(
|
||||
self, indicate, attribute, value=None, force=False
|
||||
):
|
||||
self,
|
||||
indicate: bool,
|
||||
attribute: Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
# Get all the connections for which there's at least one subscription
|
||||
connections = [
|
||||
connection
|
||||
@@ -450,13 +485,23 @@ class Server(EventEmitter):
|
||||
]
|
||||
)
|
||||
|
||||
async def notify_subscribers(self, attribute, value=None, force=False):
|
||||
async def notify_subscribers(
|
||||
self,
|
||||
attribute: Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
return await self.notify_or_indicate_subscribers(False, attribute, value, force)
|
||||
|
||||
async def indicate_subscribers(self, attribute, value=None, force=False):
|
||||
async def indicate_subscribers(
|
||||
self,
|
||||
attribute: Attribute,
|
||||
value: Optional[bytes] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
return await self.notify_or_indicate_subscribers(True, attribute, value, force)
|
||||
|
||||
def on_disconnection(self, connection):
|
||||
def on_disconnection(self, connection: Connection) -> None:
|
||||
if connection.handle in self.subscribers:
|
||||
del self.subscribers[connection.handle]
|
||||
if connection.handle in self.indication_semaphores:
|
||||
@@ -464,7 +509,7 @@ class Server(EventEmitter):
|
||||
if connection.handle in self.pending_confirmations:
|
||||
del self.pending_confirmations[connection.handle]
|
||||
|
||||
def on_gatt_pdu(self, connection, att_pdu):
|
||||
def on_gatt_pdu(self, connection: Connection, att_pdu: ATT_PDU) -> None:
|
||||
logger.debug(f'GATT Request to server: [0x{connection.handle:04X}] {att_pdu}')
|
||||
handler_name = f'on_{att_pdu.name.lower()}'
|
||||
handler = getattr(self, handler_name, None)
|
||||
@@ -506,7 +551,7 @@ class Server(EventEmitter):
|
||||
#######################################################
|
||||
# ATT handlers
|
||||
#######################################################
|
||||
def on_att_request(self, connection, pdu):
|
||||
def on_att_request(self, connection: Connection, pdu: ATT_PDU) -> None:
|
||||
'''
|
||||
Handler for requests without a more specific handler
|
||||
'''
|
||||
@@ -679,7 +724,6 @@ class Server(EventEmitter):
|
||||
and attribute.handle <= request.ending_handle
|
||||
and pdu_space_available
|
||||
):
|
||||
|
||||
try:
|
||||
attribute_value = attribute.read_value(connection)
|
||||
except ATT_Error as error:
|
||||
|
||||
933
bumble/hci.py
933
bumble/hci.py
File diff suppressed because it is too large
Load Diff
754
bumble/hfp.py
754
bumble/hfp.py
@@ -1,4 +1,4 @@
|
||||
# Copyright 2021-2022 Google LLC
|
||||
# Copyright 2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,19 +15,51 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import collections.abc
|
||||
import logging
|
||||
import asyncio
|
||||
import collections
|
||||
from typing import Union
|
||||
import dataclasses
|
||||
import enum
|
||||
import traceback
|
||||
import warnings
|
||||
from typing import Dict, List, Union, Set, TYPE_CHECKING
|
||||
|
||||
from . import at
|
||||
from . import rfcomm
|
||||
from .colors import color
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
ProtocolError,
|
||||
BT_GENERIC_AUDIO_SERVICE,
|
||||
BT_HANDSFREE_SERVICE,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
)
|
||||
from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Error
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class HfpProtocolError(ProtocolError):
|
||||
def __init__(self, error_name: str = '', details: str = ''):
|
||||
super().__init__(None, 'hfp', error_name, details)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Protocol Support
|
||||
@@ -41,6 +73,7 @@ class HfpProtocol:
|
||||
lines_available: asyncio.Event
|
||||
|
||||
def __init__(self, dlc: rfcomm.DLC) -> None:
|
||||
warnings.warn("See HfProtocol", DeprecationWarning)
|
||||
self.dlc = dlc
|
||||
self.buffer = ''
|
||||
self.lines = collections.deque()
|
||||
@@ -83,19 +116,706 @@ class HfpProtocol:
|
||||
logger.debug(color(f'<<< {line}', 'green'))
|
||||
return line
|
||||
|
||||
async def initialize_service(self) -> None:
|
||||
# Perform Service Level Connection Initialization
|
||||
self.send_command_line('AT+BRSF=2072') # Retrieve Supported Features
|
||||
await (self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
self.send_command_line('AT+CIND=?')
|
||||
await (self.next_line())
|
||||
await (self.next_line())
|
||||
# -----------------------------------------------------------------------------
|
||||
# Normative protocol definitions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
self.send_command_line('AT+CIND?')
|
||||
await (self.next_line())
|
||||
await (self.next_line())
|
||||
|
||||
self.send_command_line('AT+CMER=3,0,0,1')
|
||||
await (self.next_line())
|
||||
# HF supported features (AT+BRSF=) (normative).
|
||||
# Hands-Free Profile v1.8, 4.34.2, AT Capabilities Re-Used from GSM 07.07
|
||||
# and 3GPP 27.007
|
||||
class HfFeature(enum.IntFlag):
|
||||
EC_NR = 0x001 # Echo Cancel & Noise reduction
|
||||
THREE_WAY_CALLING = 0x002
|
||||
CLI_PRESENTATION_CAPABILITY = 0x004
|
||||
VOICE_RECOGNITION_ACTIVATION = 0x008
|
||||
REMOTE_VOLUME_CONTROL = 0x010
|
||||
ENHANCED_CALL_STATUS = 0x020
|
||||
ENHANCED_CALL_CONTROL = 0x040
|
||||
CODEC_NEGOTIATION = 0x080
|
||||
HF_INDICATORS = 0x100
|
||||
ESCO_S4_SETTINGS_SUPPORTED = 0x200
|
||||
ENHANCED_VOICE_RECOGNITION_STATUS = 0x400
|
||||
VOICE_RECOGNITION_TEST = 0x800
|
||||
|
||||
|
||||
# AG supported features (+BRSF:) (normative).
|
||||
# Hands-Free Profile v1.8, 4.34.2, AT Capabilities Re-Used from GSM 07.07
|
||||
# and 3GPP 27.007
|
||||
class AgFeature(enum.IntFlag):
|
||||
THREE_WAY_CALLING = 0x001
|
||||
EC_NR = 0x002 # Echo Cancel & Noise reduction
|
||||
VOICE_RECOGNITION_FUNCTION = 0x004
|
||||
IN_BAND_RING_TONE_CAPABILITY = 0x008
|
||||
VOICE_TAG = 0x010 # Attach a number to voice tag
|
||||
REJECT_CALL = 0x020 # Ability to reject a call
|
||||
ENHANCED_CALL_STATUS = 0x040
|
||||
ENHANCED_CALL_CONTROL = 0x080
|
||||
EXTENDED_ERROR_RESULT_CODES = 0x100
|
||||
CODEC_NEGOTIATION = 0x200
|
||||
HF_INDICATORS = 0x400
|
||||
ESCO_S4_SETTINGS_SUPPORTED = 0x800
|
||||
ENHANCED_VOICE_RECOGNITION_STATUS = 0x1000
|
||||
VOICE_RECOGNITION_TEST = 0x2000
|
||||
|
||||
|
||||
# Audio Codec IDs (normative).
|
||||
# Hands-Free Profile v1.8, 10 Appendix B
|
||||
class AudioCodec(enum.IntEnum):
|
||||
CVSD = 0x01 # Support for CVSD audio codec
|
||||
MSBC = 0x02 # Support for mSBC audio codec
|
||||
|
||||
|
||||
# HF Indicators (normative).
|
||||
# Bluetooth Assigned Numbers, 6.10.1 HF Indicators
|
||||
class HfIndicator(enum.IntEnum):
|
||||
ENHANCED_SAFETY = 0x01 # Enhanced safety feature
|
||||
BATTERY_LEVEL = 0x02 # Battery level feature
|
||||
|
||||
|
||||
# Call Hold supported operations (normative).
|
||||
# AT Commands Reference Guide, 3.5.2.3.12 +CHLD - Call Holding Services
|
||||
class CallHoldOperation(enum.IntEnum):
|
||||
RELEASE_ALL_HELD_CALLS = 0 # Release all held calls
|
||||
RELEASE_ALL_ACTIVE_CALLS = 1 # Release all active calls, accept other
|
||||
HOLD_ALL_ACTIVE_CALLS = 2 # Place all active calls on hold, accept other
|
||||
ADD_HELD_CALL = 3 # Adds a held call to conversation
|
||||
|
||||
|
||||
# Response Hold status (normative).
|
||||
# Hands-Free Profile v1.8, 4.34.2, AT Capabilities Re-Used from GSM 07.07
|
||||
# and 3GPP 27.007
|
||||
class ResponseHoldStatus(enum.IntEnum):
|
||||
INC_CALL_HELD = 0 # Put incoming call on hold
|
||||
HELD_CALL_ACC = 1 # Accept a held incoming call
|
||||
HELD_CALL_REJ = 2 # Reject a held incoming call
|
||||
|
||||
|
||||
# Values for the Call Setup AG indicator (normative).
|
||||
# Hands-Free Profile v1.8, 4.34.2, AT Capabilities Re-Used from GSM 07.07
|
||||
# and 3GPP 27.007
|
||||
class CallSetupAgIndicator(enum.IntEnum):
|
||||
NOT_IN_CALL_SETUP = 0
|
||||
INCOMING_CALL_PROCESS = 1
|
||||
OUTGOING_CALL_SETUP = 2
|
||||
REMOTE_ALERTED = 3 # Remote party alerted in an outgoing call
|
||||
|
||||
|
||||
# Values for the Call Held AG indicator (normative).
|
||||
# Hands-Free Profile v1.8, 4.34.2, AT Capabilities Re-Used from GSM 07.07
|
||||
# and 3GPP 27.007
|
||||
class CallHeldAgIndicator(enum.IntEnum):
|
||||
NO_CALLS_HELD = 0
|
||||
# Call is placed on hold or active/held calls swapped
|
||||
# (The AG has both an active AND a held call)
|
||||
CALL_ON_HOLD_AND_ACTIVE_CALL = 1
|
||||
CALL_ON_HOLD_NO_ACTIVE_CALL = 2 # Call on hold, no active call
|
||||
|
||||
|
||||
# Call Info direction (normative).
|
||||
# AT Commands Reference Guide, 3.5.2.3.15 +CLCC - List Current Calls
|
||||
class CallInfoDirection(enum.IntEnum):
|
||||
MOBILE_ORIGINATED_CALL = 0
|
||||
MOBILE_TERMINATED_CALL = 1
|
||||
|
||||
|
||||
# Call Info status (normative).
|
||||
# AT Commands Reference Guide, 3.5.2.3.15 +CLCC - List Current Calls
|
||||
class CallInfoStatus(enum.IntEnum):
|
||||
ACTIVE = 0
|
||||
HELD = 1
|
||||
DIALING = 2
|
||||
ALERTING = 3
|
||||
INCOMING = 4
|
||||
WAITING = 5
|
||||
|
||||
|
||||
# Call Info mode (normative).
|
||||
# AT Commands Reference Guide, 3.5.2.3.15 +CLCC - List Current Calls
|
||||
class CallInfoMode(enum.IntEnum):
|
||||
VOICE = 0
|
||||
DATA = 1
|
||||
FAX = 2
|
||||
UNKNOWN = 9
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Hands-Free Control Interoperability Requirements
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Response codes.
|
||||
RESPONSE_CODES = [
|
||||
"+APLSIRI",
|
||||
"+BAC",
|
||||
"+BCC",
|
||||
"+BCS",
|
||||
"+BIA",
|
||||
"+BIEV",
|
||||
"+BIND",
|
||||
"+BINP",
|
||||
"+BLDN",
|
||||
"+BRSF",
|
||||
"+BTRH",
|
||||
"+BVRA",
|
||||
"+CCWA",
|
||||
"+CHLD",
|
||||
"+CHUP",
|
||||
"+CIND",
|
||||
"+CLCC",
|
||||
"+CLIP",
|
||||
"+CMEE",
|
||||
"+CMER",
|
||||
"+CNUM",
|
||||
"+COPS",
|
||||
"+IPHONEACCEV",
|
||||
"+NREC",
|
||||
"+VGM",
|
||||
"+VGS",
|
||||
"+VTS",
|
||||
"+XAPL",
|
||||
"A",
|
||||
"D",
|
||||
]
|
||||
|
||||
# Unsolicited responses and statuses.
|
||||
UNSOLICITED_CODES = [
|
||||
"+APLSIRI",
|
||||
"+BCS",
|
||||
"+BIND",
|
||||
"+BSIR",
|
||||
"+BTRH",
|
||||
"+BVRA",
|
||||
"+CCWA",
|
||||
"+CIEV",
|
||||
"+CLIP",
|
||||
"+VGM",
|
||||
"+VGS",
|
||||
"BLACKLISTED",
|
||||
"BUSY",
|
||||
"DELAYED",
|
||||
"NO ANSWER",
|
||||
"NO CARRIER",
|
||||
"RING",
|
||||
]
|
||||
|
||||
# Status codes
|
||||
STATUS_CODES = [
|
||||
"+CME ERROR",
|
||||
"BLACKLISTED",
|
||||
"BUSY",
|
||||
"DELAYED",
|
||||
"ERROR",
|
||||
"NO ANSWER",
|
||||
"NO CARRIER",
|
||||
"OK",
|
||||
]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Configuration:
|
||||
supported_hf_features: List[HfFeature]
|
||||
supported_hf_indicators: List[HfIndicator]
|
||||
supported_audio_codecs: List[AudioCodec]
|
||||
|
||||
|
||||
class AtResponseType(enum.Enum):
|
||||
"""Indicate if a response is expected from an AT command, and if multiple
|
||||
responses are accepted."""
|
||||
|
||||
NONE = 0
|
||||
SINGLE = 1
|
||||
MULTIPLE = 2
|
||||
|
||||
|
||||
class AtResponse:
|
||||
code: str
|
||||
parameters: list
|
||||
|
||||
def __init__(self, response: bytearray):
|
||||
code_and_parameters = response.split(b':')
|
||||
parameters = (
|
||||
code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray()
|
||||
)
|
||||
self.code = code_and_parameters[0].decode()
|
||||
self.parameters = at.parse_parameters(parameters)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AgIndicatorState:
|
||||
description: str
|
||||
index: int
|
||||
supported_values: Set[int]
|
||||
current_status: int
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class HfIndicatorState:
|
||||
supported: bool = False
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
class HfProtocol:
|
||||
"""Implementation for the Hands-Free side of the Hands-Free profile.
|
||||
Reference specification Hands-Free Profile v1.8"""
|
||||
|
||||
supported_hf_features: int
|
||||
supported_audio_codecs: List[AudioCodec]
|
||||
|
||||
supported_ag_features: int
|
||||
supported_ag_call_hold_operations: List[CallHoldOperation]
|
||||
|
||||
ag_indicators: List[AgIndicatorState]
|
||||
hf_indicators: Dict[HfIndicator, HfIndicatorState]
|
||||
|
||||
dlc: rfcomm.DLC
|
||||
command_lock: asyncio.Lock
|
||||
if TYPE_CHECKING:
|
||||
response_queue: asyncio.Queue[AtResponse]
|
||||
unsolicited_queue: asyncio.Queue[AtResponse]
|
||||
else:
|
||||
response_queue: asyncio.Queue
|
||||
unsolicited_queue: asyncio.Queue
|
||||
read_buffer: bytearray
|
||||
|
||||
def __init__(self, dlc: rfcomm.DLC, configuration: Configuration):
|
||||
# Configure internal state.
|
||||
self.dlc = dlc
|
||||
self.command_lock = asyncio.Lock()
|
||||
self.response_queue = asyncio.Queue()
|
||||
self.unsolicited_queue = asyncio.Queue()
|
||||
self.read_buffer = bytearray()
|
||||
|
||||
# Build local features.
|
||||
self.supported_hf_features = sum(configuration.supported_hf_features)
|
||||
self.supported_audio_codecs = configuration.supported_audio_codecs
|
||||
|
||||
self.hf_indicators = {
|
||||
indicator: HfIndicatorState()
|
||||
for indicator in configuration.supported_hf_indicators
|
||||
}
|
||||
|
||||
# Clear remote features.
|
||||
self.supported_ag_features = 0
|
||||
self.supported_ag_call_hold_operations = []
|
||||
self.ag_indicators = []
|
||||
|
||||
# Bind the AT reader to the RFCOMM channel.
|
||||
self.dlc.sink = self._read_at
|
||||
|
||||
def supports_hf_feature(self, feature: HfFeature) -> bool:
|
||||
return (self.supported_hf_features & feature) != 0
|
||||
|
||||
def supports_ag_feature(self, feature: AgFeature) -> bool:
|
||||
return (self.supported_ag_features & feature) != 0
|
||||
|
||||
# Read AT messages from the RFCOMM channel.
|
||||
# Enqueue AT commands, responses, unsolicited responses to their
|
||||
# respective queues, and set the corresponding event.
|
||||
def _read_at(self, data: bytes):
|
||||
# Append to the read buffer.
|
||||
self.read_buffer.extend(data)
|
||||
|
||||
# Locate header and trailer.
|
||||
header = self.read_buffer.find(b'\r\n')
|
||||
trailer = self.read_buffer.find(b'\r\n', header + 2)
|
||||
if header == -1 or trailer == -1:
|
||||
return
|
||||
|
||||
# Isolate the AT response code and parameters.
|
||||
raw_response = self.read_buffer[header + 2 : trailer]
|
||||
response = AtResponse(raw_response)
|
||||
logger.debug(f"<<< {raw_response.decode()}")
|
||||
|
||||
# Consume the response bytes.
|
||||
self.read_buffer = self.read_buffer[trailer + 2 :]
|
||||
|
||||
# Forward the received code to the correct queue.
|
||||
if self.command_lock.locked() and (
|
||||
response.code in STATUS_CODES or response.code in RESPONSE_CODES
|
||||
):
|
||||
self.response_queue.put_nowait(response)
|
||||
elif response.code in UNSOLICITED_CODES:
|
||||
self.unsolicited_queue.put_nowait(response)
|
||||
else:
|
||||
logger.warning(f"dropping unexpected response with code '{response.code}'")
|
||||
|
||||
# Send an AT command and wait for the peer response.
|
||||
# Wait for the AT responses sent by the peer, to the status code.
|
||||
# Raises asyncio.TimeoutError if the status is not received
|
||||
# after a timeout (default 1 second).
|
||||
# Raises ProtocolError if the status is not OK.
|
||||
async def execute_command(
|
||||
self,
|
||||
cmd: str,
|
||||
timeout: float = 1.0,
|
||||
response_type: AtResponseType = AtResponseType.NONE,
|
||||
) -> Union[None, AtResponse, List[AtResponse]]:
|
||||
async with self.command_lock:
|
||||
logger.debug(f">>> {cmd}")
|
||||
self.dlc.write(cmd + '\r')
|
||||
responses: List[AtResponse] = []
|
||||
|
||||
while True:
|
||||
result = await asyncio.wait_for(
|
||||
self.response_queue.get(), timeout=timeout
|
||||
)
|
||||
if result.code == 'OK':
|
||||
if response_type == AtResponseType.SINGLE and len(responses) != 1:
|
||||
raise HfpProtocolError("NO ANSWER")
|
||||
|
||||
if response_type == AtResponseType.MULTIPLE:
|
||||
return responses
|
||||
if response_type == AtResponseType.SINGLE:
|
||||
return responses[0]
|
||||
return None
|
||||
if result.code in STATUS_CODES:
|
||||
raise HfpProtocolError(result.code)
|
||||
responses.append(result)
|
||||
|
||||
# 4.2.1 Service Level Connection Initialization.
|
||||
async def initiate_slc(self):
|
||||
# 4.2.1.1 Supported features exchange
|
||||
# First, in the initialization procedure, the HF shall send the
|
||||
# AT+BRSF=<HF supported features> command to the AG to both notify
|
||||
# the AG of the supported features in the HF, as well as to retrieve the
|
||||
# supported features in the AG using the +BRSF result code.
|
||||
response = await self.execute_command(
|
||||
f"AT+BRSF={self.supported_hf_features}", response_type=AtResponseType.SINGLE
|
||||
)
|
||||
|
||||
self.supported_ag_features = int(response.parameters[0])
|
||||
logger.info(f"supported AG features: {self.supported_ag_features}")
|
||||
for feature in AgFeature:
|
||||
if self.supports_ag_feature(feature):
|
||||
logger.info(f" - {feature.name}")
|
||||
|
||||
# 4.2.1.2 Codec Negotiation
|
||||
# Secondly, in the initialization procedure, if the HF supports the
|
||||
# Codec Negotiation feature, it shall check if the AT+BRSF command
|
||||
# response from the AG has indicated that it supports the Codec
|
||||
# Negotiation feature.
|
||||
if self.supports_hf_feature(
|
||||
HfFeature.CODEC_NEGOTIATION
|
||||
) and self.supports_ag_feature(AgFeature.CODEC_NEGOTIATION):
|
||||
# If both the HF and AG do support the Codec Negotiation feature
|
||||
# then the HF shall send the AT+BAC=<HF available codecs> command to
|
||||
# the AG to notify the AG of the available codecs in the HF.
|
||||
codecs = [str(c) for c in self.supported_audio_codecs]
|
||||
await self.execute_command(f"AT+BAC={','.join(codecs)}")
|
||||
|
||||
# 4.2.1.3 AG Indicators
|
||||
# After having retrieved the supported features in the AG, the HF shall
|
||||
# determine which indicators are supported by the AG, as well as the
|
||||
# ordering of the supported indicators. This is because, according to
|
||||
# the 3GPP 27.007 specification [2], the AG may support additional
|
||||
# indicators not provided for by the Hands-Free Profile, and because the
|
||||
# ordering of the indicators is implementation specific. The HF uses
|
||||
# the AT+CIND=? Test command to retrieve information about the supported
|
||||
# indicators and their ordering.
|
||||
response = await self.execute_command(
|
||||
"AT+CIND=?", response_type=AtResponseType.SINGLE
|
||||
)
|
||||
|
||||
self.ag_indicators = []
|
||||
for index, indicator in enumerate(response.parameters):
|
||||
description = indicator[0].decode()
|
||||
supported_values = []
|
||||
for value in indicator[1]:
|
||||
value = value.split(b'-')
|
||||
value = [int(v) for v in value]
|
||||
value_min = value[0]
|
||||
value_max = value[1] if len(value) > 1 else value[0]
|
||||
supported_values.extend([v for v in range(value_min, value_max + 1)])
|
||||
|
||||
self.ag_indicators.append(
|
||||
AgIndicatorState(description, index, set(supported_values), 0)
|
||||
)
|
||||
|
||||
# Once the HF has the necessary supported indicator and ordering
|
||||
# information, it shall retrieve the current status of the indicators
|
||||
# in the AG using the AT+CIND? Read command.
|
||||
response = await self.execute_command(
|
||||
"AT+CIND?", response_type=AtResponseType.SINGLE
|
||||
)
|
||||
|
||||
for index, indicator in enumerate(response.parameters):
|
||||
self.ag_indicators[index].current_status = int(indicator)
|
||||
|
||||
# After having retrieved the status of the indicators in the AG, the HF
|
||||
# shall then enable the "Indicators status update" function in the AG by
|
||||
# issuing the AT+CMER command, to which the AG shall respond with OK.
|
||||
await self.execute_command("AT+CMER=3,,,1")
|
||||
|
||||
if self.supports_hf_feature(
|
||||
HfFeature.THREE_WAY_CALLING
|
||||
) and self.supports_ag_feature(HfFeature.THREE_WAY_CALLING):
|
||||
# After the HF has enabled the “Indicators status update” function in
|
||||
# the AG, and if the “Call waiting and 3-way calling” bit was set in the
|
||||
# supported features bitmap by both the HF and the AG, the HF shall
|
||||
# issue the AT+CHLD=? test command to retrieve the information about how
|
||||
# the call hold and multiparty services are supported in the AG. The HF
|
||||
# shall not issue the AT+CHLD=? test command in case either the HF or
|
||||
# the AG does not support the "Three-way calling" feature.
|
||||
response = await self.execute_command(
|
||||
"AT+CHLD=?", response_type=AtResponseType.SINGLE
|
||||
)
|
||||
|
||||
self.supported_ag_call_hold_operations = [
|
||||
CallHoldOperation(int(operation))
|
||||
for operation in response.parameters[0]
|
||||
if not b'x' in operation
|
||||
]
|
||||
|
||||
# 4.2.1.4 HF Indicators
|
||||
# If the HF supports the HF indicator feature, it shall check the +BRSF
|
||||
# response to see if the AG also supports the HF Indicator feature.
|
||||
if self.supports_hf_feature(
|
||||
HfFeature.HF_INDICATORS
|
||||
) and self.supports_ag_feature(AgFeature.HF_INDICATORS):
|
||||
# If both the HF and AG support the HF Indicator feature, then the HF
|
||||
# shall send the AT+BIND=<HF supported HF indicators> command to the AG
|
||||
# to notify the AG of the supported indicators’ assigned numbers in the
|
||||
# HF. The AG shall respond with OK
|
||||
indicators = [str(i) for i in self.hf_indicators.keys()]
|
||||
await self.execute_command(f"AT+BIND={','.join(indicators)}")
|
||||
|
||||
# After having provided the AG with the HF indicators it supports,
|
||||
# the HF shall send the AT+BIND=? to request HF indicators supported
|
||||
# by the AG. The AG shall reply with the +BIND response listing all
|
||||
# HF indicators that it supports followed by an OK.
|
||||
response = await self.execute_command(
|
||||
"AT+BIND=?", response_type=AtResponseType.SINGLE
|
||||
)
|
||||
|
||||
logger.info("supported HF indicators:")
|
||||
for indicator in response.parameters[0]:
|
||||
indicator = HfIndicator(int(indicator))
|
||||
logger.info(f" - {indicator.name}")
|
||||
if indicator in self.hf_indicators:
|
||||
self.hf_indicators[indicator].supported = True
|
||||
|
||||
# Once the HF receives the supported HF indicators list from the AG,
|
||||
# the HF shall send the AT+BIND? command to determine which HF
|
||||
# indicators are enabled. The AG shall respond with one or more
|
||||
# +BIND responses. The AG shall terminate the list with OK.
|
||||
# (See Section 4.36.1.3).
|
||||
responses = await self.execute_command(
|
||||
"AT+BIND?", response_type=AtResponseType.MULTIPLE
|
||||
)
|
||||
|
||||
logger.info("enabled HF indicators:")
|
||||
for response in responses:
|
||||
indicator = HfIndicator(int(response.parameters[0]))
|
||||
enabled = int(response.parameters[1]) != 0
|
||||
logger.info(f" - {indicator.name}: {enabled}")
|
||||
if indicator in self.hf_indicators:
|
||||
self.hf_indicators[indicator].enabled = True
|
||||
|
||||
logger.info("SLC setup completed")
|
||||
|
||||
# 4.11.2 Audio Connection Setup by HF
|
||||
async def setup_audio_connection(self):
|
||||
# When the HF triggers the establishment of the Codec Connection it
|
||||
# shall send the AT command AT+BCC to the AG. The AG shall respond with
|
||||
# OK if it will start the Codec Connection procedure, and with ERROR
|
||||
# if it cannot start the Codec Connection procedure.
|
||||
await self.execute_command("AT+BCC")
|
||||
|
||||
# 4.11.3 Codec Connection Setup
|
||||
async def setup_codec_connection(self, codec_id: int):
|
||||
# The AG shall send a +BCS=<Codec ID> unsolicited response to the HF.
|
||||
# The HF shall then respond to the incoming unsolicited response with
|
||||
# the AT command AT+BCS=<Codec ID>. The ID shall be the same as in the
|
||||
# unsolicited response code as long as the ID is supported.
|
||||
# If the received ID is not available, the HF shall respond with
|
||||
# AT+BAC with its available codecs.
|
||||
if codec_id not in self.supported_audio_codecs:
|
||||
codecs = [str(c) for c in self.supported_audio_codecs]
|
||||
await self.execute_command(f"AT+BAC={','.join(codecs)}")
|
||||
return
|
||||
|
||||
await self.execute_command(f"AT+BCS={codec_id}")
|
||||
|
||||
# After sending the OK response, the AG shall open the
|
||||
# Synchronous Connection with the settings that are determined by the
|
||||
# ID. The HF shall be ready to accept the synchronous connection
|
||||
# establishment as soon as it has sent the AT commands AT+BCS=<Codec ID>.
|
||||
|
||||
logger.info("codec connection setup completed")
|
||||
|
||||
# 4.13.1 Answer Incoming Call from the HF – In-Band Ringing
|
||||
async def answer_incoming_call(self):
|
||||
# The user accepts the incoming voice call by using the proper means
|
||||
# provided by the HF. The HF shall then send the ATA command
|
||||
# (see Section 4.34) to the AG. The AG shall then begin the procedure for
|
||||
# accepting the incoming call.
|
||||
await self.execute_command("ATA")
|
||||
|
||||
# 4.14.1 Reject an Incoming Call from the HF
|
||||
async def reject_incoming_call(self):
|
||||
# The user rejects the incoming call by using the User Interface on the
|
||||
# Hands-Free unit. The HF shall then send the AT+CHUP command
|
||||
# (see Section 4.34) to the AG. This may happen at any time during the
|
||||
# procedures described in Sections 4.13.1 and 4.13.2.
|
||||
await self.execute_command("AT+CHUP")
|
||||
|
||||
# 4.15.1 Terminate a Call Process from the HF
|
||||
async def terminate_call(self):
|
||||
# The user may abort the ongoing call process using whatever means
|
||||
# provided by the Hands-Free unit. The HF shall send AT+CHUP command
|
||||
# (see Section 4.34) to the AG, and the AG shall then start the
|
||||
# procedure to terminate or interrupt the current call procedure.
|
||||
# The AG shall then send the OK indication followed by the +CIEV result
|
||||
# code, with the value indicating (call=0).
|
||||
await self.execute_command("AT+CHUP")
|
||||
|
||||
async def update_ag_indicator(self, index: int, value: int):
|
||||
self.ag_indicators[index].current_status = value
|
||||
logger.info(
|
||||
f"AG indicator updated: {self.ag_indicators[index].description}, {value}"
|
||||
)
|
||||
|
||||
async def handle_unsolicited(self):
|
||||
"""Handle unsolicited result codes sent by the audio gateway."""
|
||||
result = await self.unsolicited_queue.get()
|
||||
if result.code == "+BCS":
|
||||
await self.setup_codec_connection(int(result.parameters[0]))
|
||||
elif result.code == "+CIEV":
|
||||
await self.update_ag_indicator(
|
||||
int(result.parameters[0]), int(result.parameters[1])
|
||||
)
|
||||
else:
|
||||
logging.info(f"unhandled unsolicited response {result.code}")
|
||||
|
||||
async def run(self):
|
||||
"""Main rountine for the Hands-Free side of the HFP protocol.
|
||||
Initiates the service level connection then loops handling
|
||||
unsolicited AG responses."""
|
||||
|
||||
try:
|
||||
await self.initiate_slc()
|
||||
while True:
|
||||
await self.handle_unsolicited()
|
||||
except Exception:
|
||||
logger.error("HFP-HF protocol failed with the following error:")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Normative SDP definitions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Profile version (normative).
|
||||
# Hands-Free Profile v1.8, 5.3 SDP Interoperability Requirements
|
||||
class ProfileVersion(enum.IntEnum):
|
||||
V1_5 = 0x0105
|
||||
V1_6 = 0x0106
|
||||
V1_7 = 0x0107
|
||||
V1_8 = 0x0108
|
||||
V1_9 = 0x0109
|
||||
|
||||
|
||||
# HF supported features (normative).
|
||||
# Hands-Free Profile v1.8, 5.3 SDP Interoperability Requirements
|
||||
class HfSdpFeature(enum.IntFlag):
|
||||
EC_NR = 0x01 # Echo Cancel & Noise reduction
|
||||
THREE_WAY_CALLING = 0x02
|
||||
CLI_PRESENTATION_CAPABILITY = 0x04
|
||||
VOICE_RECOGNITION_ACTIVATION = 0x08
|
||||
REMOTE_VOLUME_CONTROL = 0x10
|
||||
WIDE_BAND = 0x20 # Wide band speech
|
||||
ENHANCED_VOICE_RECOGNITION_STATUS = 0x40
|
||||
VOICE_RECOGNITION_TEST = 0x80
|
||||
|
||||
|
||||
# AG supported features (normative).
|
||||
# Hands-Free Profile v1.8, 5.3 SDP Interoperability Requirements
|
||||
class AgSdpFeature(enum.IntFlag):
|
||||
THREE_WAY_CALLING = 0x01
|
||||
EC_NR = 0x02 # Echo Cancel & Noise reduction
|
||||
VOICE_RECOGNITION_FUNCTION = 0x04
|
||||
IN_BAND_RING_TONE_CAPABILITY = 0x08
|
||||
VOICE_TAG = 0x10 # Attach a number to voice tag
|
||||
WIDE_BAND = 0x20 # Wide band speech
|
||||
ENHANCED_VOICE_RECOGNITION_STATUS = 0x40
|
||||
VOICE_RECOGNITION_TEST = 0x80
|
||||
|
||||
|
||||
def sdp_records(
|
||||
service_record_handle: int, rfcomm_channel: int, configuration: Configuration
|
||||
) -> List[ServiceAttribute]:
|
||||
"""Generate the SDP record for HFP Hands-Free support.
|
||||
The record exposes the features supported in the input configuration,
|
||||
and the allocated RFCOMM channel."""
|
||||
|
||||
hf_supported_features = 0
|
||||
|
||||
if HfFeature.EC_NR in configuration.supported_hf_features:
|
||||
hf_supported_features |= HfSdpFeature.EC_NR
|
||||
if HfFeature.THREE_WAY_CALLING in configuration.supported_hf_features:
|
||||
hf_supported_features |= HfSdpFeature.THREE_WAY_CALLING
|
||||
if HfFeature.CLI_PRESENTATION_CAPABILITY in configuration.supported_hf_features:
|
||||
hf_supported_features |= HfSdpFeature.CLI_PRESENTATION_CAPABILITY
|
||||
if HfFeature.VOICE_RECOGNITION_ACTIVATION in configuration.supported_hf_features:
|
||||
hf_supported_features |= HfSdpFeature.VOICE_RECOGNITION_ACTIVATION
|
||||
if HfFeature.REMOTE_VOLUME_CONTROL in configuration.supported_hf_features:
|
||||
hf_supported_features |= HfSdpFeature.REMOTE_VOLUME_CONTROL
|
||||
if (
|
||||
HfFeature.ENHANCED_VOICE_RECOGNITION_STATUS
|
||||
in configuration.supported_hf_features
|
||||
):
|
||||
hf_supported_features |= HfSdpFeature.ENHANCED_VOICE_RECOGNITION_STATUS
|
||||
if HfFeature.VOICE_RECOGNITION_TEST in configuration.supported_hf_features:
|
||||
hf_supported_features |= HfSdpFeature.VOICE_RECOGNITION_TEST
|
||||
|
||||
if AudioCodec.MSBC in configuration.supported_audio_codecs:
|
||||
hf_supported_features |= HfSdpFeature.WIDE_BAND
|
||||
|
||||
return [
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(service_record_handle),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_HANDSFREE_SERVICE),
|
||||
DataElement.uuid(BT_GENERIC_AUDIO_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence([DataElement.uuid(BT_L2CAP_PROTOCOL_ID)]),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_8(rfcomm_channel),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_HANDSFREE_SERVICE),
|
||||
DataElement.unsigned_integer_16(ProfileVersion.V1_8),
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_16(hf_supported_features),
|
||||
),
|
||||
]
|
||||
|
||||
109
bumble/host.py
109
bumble/host.py
@@ -15,22 +15,24 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import collections
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from typing import Optional, TYPE_CHECKING, Dict, Callable, Awaitable
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
from bumble.snoop import Snooper
|
||||
|
||||
from typing import Optional
|
||||
from bumble import drivers
|
||||
|
||||
from .hci import (
|
||||
Address,
|
||||
HCI_ACL_DATA_PACKET,
|
||||
HCI_COMMAND_COMPLETE_EVENT,
|
||||
HCI_COMMAND_PACKET,
|
||||
HCI_COMMAND_COMPLETE_EVENT,
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
||||
HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND,
|
||||
@@ -44,8 +46,11 @@ from .hci import (
|
||||
HCI_VERSION_BLUETOOTH_CORE_4_0,
|
||||
HCI_AclDataPacket,
|
||||
HCI_AclDataPacketAssembler,
|
||||
HCI_Command,
|
||||
HCI_Command_Complete_Event,
|
||||
HCI_Constant,
|
||||
HCI_Error,
|
||||
HCI_Event,
|
||||
HCI_LE_Long_Term_Key_Request_Negative_Reply_Command,
|
||||
HCI_LE_Long_Term_Key_Request_Reply_Command,
|
||||
HCI_LE_Read_Buffer_Size_Command,
|
||||
@@ -62,16 +67,19 @@ from .hci import (
|
||||
HCI_Read_Local_Version_Information_Command,
|
||||
HCI_Reset_Command,
|
||||
HCI_Set_Event_Mask_Command,
|
||||
map_null_terminated_utf8_string,
|
||||
)
|
||||
from .core import (
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_CENTRAL_ROLE,
|
||||
BT_LE_TRANSPORT,
|
||||
ConnectionPHY,
|
||||
ConnectionParameters,
|
||||
InvalidStateError,
|
||||
)
|
||||
from .utils import AbortableEventEmitter
|
||||
from .transport.common import TransportLostError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .transport.common import TransportSink, TransportSource
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -95,27 +103,39 @@ HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS = 1
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Connection:
|
||||
def __init__(self, host, handle, peer_address, transport):
|
||||
def __init__(self, host: Host, handle: int, peer_address: Address, transport: int):
|
||||
self.host = host
|
||||
self.handle = handle
|
||||
self.peer_address = peer_address
|
||||
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||
self.transport = transport
|
||||
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
def on_hci_acl_data_packet(self, packet: HCI_AclDataPacket) -> None:
|
||||
self.assembler.feed_packet(packet)
|
||||
|
||||
def on_acl_pdu(self, pdu):
|
||||
def on_acl_pdu(self, pdu: bytes) -> None:
|
||||
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
||||
self.host.on_l2cap_pdu(self, l2cap_pdu.cid, l2cap_pdu.payload)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Host(AbortableEventEmitter):
|
||||
def __init__(self, controller_source=None, controller_sink=None):
|
||||
connections: Dict[int, Connection]
|
||||
acl_packet_queue: collections.deque[HCI_AclDataPacket]
|
||||
hci_sink: TransportSink
|
||||
long_term_key_provider: Optional[
|
||||
Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
|
||||
]
|
||||
link_key_provider: Optional[Callable[[Address], Awaitable[Optional[bytes]]]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller_source: Optional[TransportSource] = None,
|
||||
controller_sink: Optional[TransportSink] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.hci_sink = None
|
||||
self.hci_metadata = None
|
||||
self.ready = False # True when we can accept incoming packets
|
||||
self.reset_done = False
|
||||
self.connections = {} # Connections, by connection handle
|
||||
@@ -141,6 +161,9 @@ class Host(AbortableEventEmitter):
|
||||
# Connect to the source and sink if specified
|
||||
if controller_source:
|
||||
controller_source.set_packet_sink(self)
|
||||
self.hci_metadata = getattr(
|
||||
controller_source, 'metadata', self.hci_metadata
|
||||
)
|
||||
if controller_sink:
|
||||
self.set_packet_sink(controller_sink)
|
||||
|
||||
@@ -170,7 +193,7 @@ class Host(AbortableEventEmitter):
|
||||
self.emit('flush')
|
||||
self.command_semaphore.release()
|
||||
|
||||
async def reset(self):
|
||||
async def reset(self, driver_factory=drivers.get_driver_for_host):
|
||||
if self.ready:
|
||||
self.ready = False
|
||||
await self.flush()
|
||||
@@ -178,6 +201,15 @@ class Host(AbortableEventEmitter):
|
||||
await self.send_command(HCI_Reset_Command(), check_result=True)
|
||||
self.ready = True
|
||||
|
||||
# Instantiate and init a driver for the host if needed.
|
||||
# NOTE: we don't keep a reference to the driver here, because we don't
|
||||
# currently have a need for the driver later on. But if the driver interface
|
||||
# evolves, it may be required, then, to store a reference to the driver in
|
||||
# an object property.
|
||||
if driver_factory is not None:
|
||||
if driver := await driver_factory(self):
|
||||
await driver.init_controller()
|
||||
|
||||
response = await self.send_command(
|
||||
HCI_Read_Local_Supported_Commands_Command(), check_result=True
|
||||
)
|
||||
@@ -282,7 +314,7 @@ class Host(AbortableEventEmitter):
|
||||
self.reset_done = True
|
||||
|
||||
@property
|
||||
def controller(self):
|
||||
def controller(self) -> TransportSink:
|
||||
return self.hci_sink
|
||||
|
||||
@controller.setter
|
||||
@@ -291,14 +323,13 @@ class Host(AbortableEventEmitter):
|
||||
if controller:
|
||||
controller.set_packet_sink(self)
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
def set_packet_sink(self, sink: TransportSink) -> None:
|
||||
self.hci_sink = sink
|
||||
|
||||
def send_hci_packet(self, packet):
|
||||
def send_hci_packet(self, packet: HCI_Packet) -> None:
|
||||
if self.snooper:
|
||||
self.snooper.snoop(bytes(packet), Snooper.Direction.HOST_TO_CONTROLLER)
|
||||
|
||||
self.hci_sink.on_packet(packet.to_bytes())
|
||||
self.hci_sink.on_packet(bytes(packet))
|
||||
|
||||
async def send_command(self, command, check_result=False):
|
||||
logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {command}')
|
||||
@@ -335,7 +366,7 @@ class Host(AbortableEventEmitter):
|
||||
return response
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f'{color("!!! Exception while sending HCI packet:", "red")} {error}'
|
||||
f'{color("!!! Exception while sending command:", "red")} {error}'
|
||||
)
|
||||
raise error
|
||||
finally:
|
||||
@@ -343,14 +374,14 @@ class Host(AbortableEventEmitter):
|
||||
self.pending_response = None
|
||||
|
||||
# Use this method to send a command from a task
|
||||
def send_command_sync(self, command):
|
||||
async def send_command(command):
|
||||
def send_command_sync(self, command: HCI_Command) -> None:
|
||||
async def send_command(command: HCI_Command) -> None:
|
||||
await self.send_command(command)
|
||||
|
||||
asyncio.create_task(send_command(command))
|
||||
|
||||
def send_l2cap_pdu(self, connection_handle, cid, pdu):
|
||||
l2cap_pdu = L2CAP_PDU(cid, pdu).to_bytes()
|
||||
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
||||
l2cap_pdu = bytes(L2CAP_PDU(cid, pdu))
|
||||
|
||||
# Send the data to the controller via ACL packets
|
||||
bytes_remaining = len(l2cap_pdu)
|
||||
@@ -374,7 +405,7 @@ class Host(AbortableEventEmitter):
|
||||
offset += data_total_length
|
||||
bytes_remaining -= data_total_length
|
||||
|
||||
def queue_acl_packet(self, acl_packet):
|
||||
def queue_acl_packet(self, acl_packet: HCI_AclDataPacket) -> None:
|
||||
self.acl_packet_queue.appendleft(acl_packet)
|
||||
self.check_acl_packet_queue()
|
||||
|
||||
@@ -384,7 +415,7 @@ class Host(AbortableEventEmitter):
|
||||
f'{len(self.acl_packet_queue)} in queue'
|
||||
)
|
||||
|
||||
def check_acl_packet_queue(self):
|
||||
def check_acl_packet_queue(self) -> None:
|
||||
# Send all we can (TODO: support different LE/Classic limits)
|
||||
while (
|
||||
len(self.acl_packet_queue) > 0
|
||||
@@ -430,47 +461,53 @@ class Host(AbortableEventEmitter):
|
||||
]
|
||||
|
||||
# Packet Sink protocol (packets coming from the controller via HCI)
|
||||
def on_packet(self, packet):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
hci_packet = HCI_Packet.from_bytes(packet)
|
||||
if self.ready or (
|
||||
hci_packet.hci_packet_type == HCI_EVENT_PACKET
|
||||
and hci_packet.event_code == HCI_COMMAND_COMPLETE_EVENT
|
||||
isinstance(hci_packet, HCI_Command_Complete_Event)
|
||||
and hci_packet.command_opcode == HCI_RESET_COMMAND
|
||||
):
|
||||
self.on_hci_packet(hci_packet)
|
||||
else:
|
||||
logger.debug('reset not done, ignoring packet from controller')
|
||||
|
||||
def on_hci_packet(self, packet):
|
||||
def on_transport_lost(self):
|
||||
# Called by the source when the transport has been lost.
|
||||
if self.pending_response:
|
||||
self.pending_response.set_exception(TransportLostError('transport lost'))
|
||||
|
||||
self.emit('flush')
|
||||
|
||||
def on_hci_packet(self, packet: HCI_Packet) -> None:
|
||||
logger.debug(f'{color("### CONTROLLER -> HOST", "green")}: {packet}')
|
||||
|
||||
if self.snooper:
|
||||
self.snooper.snoop(bytes(packet), Snooper.Direction.CONTROLLER_TO_HOST)
|
||||
|
||||
# If the packet is a command, invoke the handler for this packet
|
||||
if packet.hci_packet_type == HCI_COMMAND_PACKET:
|
||||
if isinstance(packet, HCI_Command):
|
||||
self.on_hci_command_packet(packet)
|
||||
elif packet.hci_packet_type == HCI_EVENT_PACKET:
|
||||
elif isinstance(packet, HCI_Event):
|
||||
self.on_hci_event_packet(packet)
|
||||
elif packet.hci_packet_type == HCI_ACL_DATA_PACKET:
|
||||
elif isinstance(packet, HCI_AclDataPacket):
|
||||
self.on_hci_acl_data_packet(packet)
|
||||
else:
|
||||
logger.warning(f'!!! unknown packet type {packet.hci_packet_type}')
|
||||
|
||||
def on_hci_command_packet(self, command):
|
||||
def on_hci_command_packet(self, command: HCI_Command) -> None:
|
||||
logger.warning(f'!!! unexpected command packet: {command}')
|
||||
|
||||
def on_hci_event_packet(self, event):
|
||||
def on_hci_event_packet(self, event: HCI_Event) -> None:
|
||||
handler_name = f'on_{event.name.lower()}'
|
||||
handler = getattr(self, handler_name, self.on_hci_event)
|
||||
handler(event)
|
||||
|
||||
def on_hci_acl_data_packet(self, packet):
|
||||
def on_hci_acl_data_packet(self, packet: HCI_AclDataPacket) -> None:
|
||||
# Look for the connection to which this data belongs
|
||||
if connection := self.connections.get(packet.connection_handle):
|
||||
connection.on_hci_acl_data_packet(packet)
|
||||
|
||||
def on_l2cap_pdu(self, connection, cid, pdu):
|
||||
def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
|
||||
self.emit('l2cap_pdu', connection.handle, cid, pdu)
|
||||
|
||||
def on_command_processed(self, event):
|
||||
@@ -808,6 +845,10 @@ class Host(AbortableEventEmitter):
|
||||
f'simple pairing complete for {event.bd_addr}: '
|
||||
f'status={HCI_Constant.status_name(event.status)}'
|
||||
)
|
||||
if event.status == HCI_SUCCESS:
|
||||
self.emit('classic_pairing', event.bd_addr)
|
||||
else:
|
||||
self.emit('classic_pairing_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_pin_code_request_event(self, event):
|
||||
self.emit('pin_code_request', event.bd_addr)
|
||||
|
||||
586
bumble/l2cap.py
586
bumble/l2cap.py
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ import enum
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from .hci import (
|
||||
Address,
|
||||
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||
@@ -168,21 +169,28 @@ class PairingDelegate:
|
||||
class PairingConfig:
|
||||
"""Configuration for the Pairing protocol."""
|
||||
|
||||
class AddressType(enum.IntEnum):
|
||||
PUBLIC = Address.PUBLIC_DEVICE_ADDRESS
|
||||
RANDOM = Address.RANDOM_DEVICE_ADDRESS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sc: bool = True,
|
||||
mitm: bool = True,
|
||||
bonding: bool = True,
|
||||
delegate: Optional[PairingDelegate] = None,
|
||||
identity_address_type: Optional[AddressType] = None,
|
||||
) -> None:
|
||||
self.sc = sc
|
||||
self.mitm = mitm
|
||||
self.bonding = bonding
|
||||
self.delegate = delegate or PairingDelegate()
|
||||
self.identity_address_type = identity_address_type
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'PairingConfig(sc={self.sc}, '
|
||||
f'mitm={self.mitm}, bonding={self.bonding}, '
|
||||
f'identity_address_type={self.identity_address_type}, '
|
||||
f'delegate[{self.delegate.io_capability}])'
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from bumble.pairing import PairingDelegate
|
||||
from bumble.pairing import PairingConfig, PairingDelegate
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict
|
||||
|
||||
@@ -20,6 +20,7 @@ from typing import Any, Dict
|
||||
@dataclass
|
||||
class Config:
|
||||
io_capability: PairingDelegate.IoCapability = PairingDelegate.NO_OUTPUT_NO_INPUT
|
||||
identity_address_type: PairingConfig.AddressType = PairingConfig.AddressType.RANDOM
|
||||
pairing_sc_enable: bool = True
|
||||
pairing_mitm_enable: bool = True
|
||||
pairing_bonding_enable: bool = True
|
||||
@@ -35,6 +36,12 @@ class Config:
|
||||
'io_capability', 'no_output_no_input'
|
||||
).upper()
|
||||
self.io_capability = getattr(PairingDelegate, io_capability_name)
|
||||
identity_address_type_name: str = config.get(
|
||||
'identity_address_type', 'random'
|
||||
).upper()
|
||||
self.identity_address_type = getattr(
|
||||
PairingConfig.AddressType, identity_address_type_name
|
||||
)
|
||||
self.pairing_sc_enable = config.get('pairing_sc_enable', True)
|
||||
self.pairing_mitm_enable = config.get('pairing_mitm_enable', True)
|
||||
self.pairing_bonding_enable = config.get('pairing_bonding_enable', True)
|
||||
|
||||
@@ -34,6 +34,10 @@ from bumble.sdp import (
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
# Default rootcanal HCI TCP address
|
||||
ROOTCANAL_HCI_ADDRESS = "localhost:6402"
|
||||
|
||||
|
||||
class PandoraDevice:
|
||||
"""
|
||||
Small wrapper around a Bumble device and it's HCI transport.
|
||||
@@ -53,7 +57,9 @@ class PandoraDevice:
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
self.device = _make_device(config)
|
||||
self._hci_name = config.get('transport', '')
|
||||
self._hci_name = config.get(
|
||||
'transport', f"tcp-client:{config.get('tcp', ROOTCANAL_HCI_ADDRESS)}"
|
||||
)
|
||||
self._hci = None
|
||||
|
||||
@property
|
||||
|
||||
@@ -112,7 +112,7 @@ class HostService(HostServicer):
|
||||
async def FactoryReset(
|
||||
self, request: empty_pb2.Empty, context: grpc.ServicerContext
|
||||
) -> empty_pb2.Empty:
|
||||
self.log.info('FactoryReset')
|
||||
self.log.debug('FactoryReset')
|
||||
|
||||
# delete all bonds
|
||||
if self.device.keystore is not None:
|
||||
@@ -126,7 +126,7 @@ class HostService(HostServicer):
|
||||
async def Reset(
|
||||
self, request: empty_pb2.Empty, context: grpc.ServicerContext
|
||||
) -> empty_pb2.Empty:
|
||||
self.log.info('Reset')
|
||||
self.log.debug('Reset')
|
||||
|
||||
# clear service.
|
||||
self.waited_connections.clear()
|
||||
@@ -139,7 +139,7 @@ class HostService(HostServicer):
|
||||
async def ReadLocalAddress(
|
||||
self, request: empty_pb2.Empty, context: grpc.ServicerContext
|
||||
) -> ReadLocalAddressResponse:
|
||||
self.log.info('ReadLocalAddress')
|
||||
self.log.debug('ReadLocalAddress')
|
||||
return ReadLocalAddressResponse(
|
||||
address=bytes(reversed(bytes(self.device.public_address)))
|
||||
)
|
||||
@@ -152,7 +152,7 @@ class HostService(HostServicer):
|
||||
address = Address(
|
||||
bytes(reversed(request.address)), address_type=Address.PUBLIC_DEVICE_ADDRESS
|
||||
)
|
||||
self.log.info(f"Connect to {address}")
|
||||
self.log.debug(f"Connect to {address}")
|
||||
|
||||
try:
|
||||
connection = await self.device.connect(
|
||||
@@ -167,7 +167,7 @@ class HostService(HostServicer):
|
||||
return ConnectResponse(connection_already_exists=empty_pb2.Empty())
|
||||
raise e
|
||||
|
||||
self.log.info(f"Connect to {address} done (handle={connection.handle})")
|
||||
self.log.debug(f"Connect to {address} done (handle={connection.handle})")
|
||||
|
||||
cookie = any_pb2.Any(value=connection.handle.to_bytes(4, 'big'))
|
||||
return ConnectResponse(connection=Connection(cookie=cookie))
|
||||
@@ -186,7 +186,7 @@ class HostService(HostServicer):
|
||||
if address in (Address.NIL, Address.ANY):
|
||||
raise ValueError('Invalid address')
|
||||
|
||||
self.log.info(f"WaitConnection from {address}...")
|
||||
self.log.debug(f"WaitConnection from {address}...")
|
||||
|
||||
connection = self.device.find_connection_by_bd_addr(
|
||||
address, transport=BT_BR_EDR_TRANSPORT
|
||||
@@ -201,7 +201,7 @@ class HostService(HostServicer):
|
||||
# save connection has waited and respond.
|
||||
self.waited_connections.add(id(connection))
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
f"WaitConnection from {address} done (handle={connection.handle})"
|
||||
)
|
||||
|
||||
@@ -216,7 +216,7 @@ class HostService(HostServicer):
|
||||
if address in (Address.NIL, Address.ANY):
|
||||
raise ValueError('Invalid address')
|
||||
|
||||
self.log.info(f"ConnectLE to {address}...")
|
||||
self.log.debug(f"ConnectLE to {address}...")
|
||||
|
||||
try:
|
||||
connection = await self.device.connect(
|
||||
@@ -233,7 +233,7 @@ class HostService(HostServicer):
|
||||
return ConnectLEResponse(connection_already_exists=empty_pb2.Empty())
|
||||
raise e
|
||||
|
||||
self.log.info(f"ConnectLE to {address} done (handle={connection.handle})")
|
||||
self.log.debug(f"ConnectLE to {address} done (handle={connection.handle})")
|
||||
|
||||
cookie = any_pb2.Any(value=connection.handle.to_bytes(4, 'big'))
|
||||
return ConnectLEResponse(connection=Connection(cookie=cookie))
|
||||
@@ -243,12 +243,12 @@ class HostService(HostServicer):
|
||||
self, request: DisconnectRequest, context: grpc.ServicerContext
|
||||
) -> empty_pb2.Empty:
|
||||
connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
|
||||
self.log.info(f"Disconnect: {connection_handle}")
|
||||
self.log.debug(f"Disconnect: {connection_handle}")
|
||||
|
||||
self.log.info("Disconnecting...")
|
||||
self.log.debug("Disconnecting...")
|
||||
if connection := self.device.lookup_connection(connection_handle):
|
||||
await connection.disconnect(HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR)
|
||||
self.log.info("Disconnected")
|
||||
self.log.debug("Disconnected")
|
||||
|
||||
return empty_pb2.Empty()
|
||||
|
||||
@@ -257,7 +257,7 @@ class HostService(HostServicer):
|
||||
self, request: WaitDisconnectionRequest, context: grpc.ServicerContext
|
||||
) -> empty_pb2.Empty:
|
||||
connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
|
||||
self.log.info(f"WaitDisconnection: {connection_handle}")
|
||||
self.log.debug(f"WaitDisconnection: {connection_handle}")
|
||||
|
||||
if connection := self.device.lookup_connection(connection_handle):
|
||||
disconnection_future: asyncio.Future[
|
||||
@@ -270,7 +270,7 @@ class HostService(HostServicer):
|
||||
connection.on('disconnection', on_disconnection)
|
||||
try:
|
||||
await disconnection_future
|
||||
self.log.info("Disconnected")
|
||||
self.log.debug("Disconnected")
|
||||
finally:
|
||||
connection.remove_listener('disconnection', on_disconnection) # type: ignore
|
||||
|
||||
@@ -378,7 +378,7 @@ class HostService(HostServicer):
|
||||
try:
|
||||
while True:
|
||||
if not self.device.is_advertising:
|
||||
self.log.info('Advertise')
|
||||
self.log.debug('Advertise')
|
||||
await self.device.start_advertising(
|
||||
target=target,
|
||||
advertising_type=advertising_type,
|
||||
@@ -393,10 +393,10 @@ class HostService(HostServicer):
|
||||
bumble.device.Connection
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
|
||||
self.log.info('Wait for LE connection...')
|
||||
self.log.debug('Wait for LE connection...')
|
||||
connection = await pending_connection
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
f"Advertise: Connected to {connection.peer_address} (handle={connection.handle})"
|
||||
)
|
||||
|
||||
@@ -410,7 +410,7 @@ class HostService(HostServicer):
|
||||
self.device.remove_listener('connection', on_connection) # type: ignore
|
||||
|
||||
try:
|
||||
self.log.info('Stop advertising')
|
||||
self.log.debug('Stop advertising')
|
||||
await self.device.abort_on('flush', self.device.stop_advertising())
|
||||
except:
|
||||
pass
|
||||
@@ -423,7 +423,7 @@ class HostService(HostServicer):
|
||||
if request.phys:
|
||||
raise NotImplementedError("TODO: add support for `request.phys`")
|
||||
|
||||
self.log.info('Scan')
|
||||
self.log.debug('Scan')
|
||||
|
||||
scan_queue: asyncio.Queue[Advertisement] = asyncio.Queue()
|
||||
handler = self.device.on('advertisement', scan_queue.put_nowait)
|
||||
@@ -470,7 +470,7 @@ class HostService(HostServicer):
|
||||
finally:
|
||||
self.device.remove_listener('advertisement', handler) # type: ignore
|
||||
try:
|
||||
self.log.info('Stop scanning')
|
||||
self.log.debug('Stop scanning')
|
||||
await self.device.abort_on('flush', self.device.stop_scanning())
|
||||
except:
|
||||
pass
|
||||
@@ -479,7 +479,7 @@ class HostService(HostServicer):
|
||||
async def Inquiry(
|
||||
self, request: empty_pb2.Empty, context: grpc.ServicerContext
|
||||
) -> AsyncGenerator[InquiryResponse, None]:
|
||||
self.log.info('Inquiry')
|
||||
self.log.debug('Inquiry')
|
||||
|
||||
inquiry_queue: asyncio.Queue[
|
||||
Optional[Tuple[Address, int, AdvertisingData, int]]
|
||||
@@ -510,7 +510,7 @@ class HostService(HostServicer):
|
||||
self.device.remove_listener('inquiry_complete', complete_handler) # type: ignore
|
||||
self.device.remove_listener('inquiry_result', result_handler) # type: ignore
|
||||
try:
|
||||
self.log.info('Stop inquiry')
|
||||
self.log.debug('Stop inquiry')
|
||||
await self.device.abort_on('flush', self.device.stop_discovery())
|
||||
except:
|
||||
pass
|
||||
@@ -519,7 +519,7 @@ class HostService(HostServicer):
|
||||
async def SetDiscoverabilityMode(
|
||||
self, request: SetDiscoverabilityModeRequest, context: grpc.ServicerContext
|
||||
) -> empty_pb2.Empty:
|
||||
self.log.info("SetDiscoverabilityMode")
|
||||
self.log.debug("SetDiscoverabilityMode")
|
||||
await self.device.set_discoverable(request.mode != NOT_DISCOVERABLE)
|
||||
return empty_pb2.Empty()
|
||||
|
||||
@@ -527,7 +527,7 @@ class HostService(HostServicer):
|
||||
async def SetConnectabilityMode(
|
||||
self, request: SetConnectabilityModeRequest, context: grpc.ServicerContext
|
||||
) -> empty_pb2.Empty:
|
||||
self.log.info("SetConnectabilityMode")
|
||||
self.log.debug("SetConnectabilityMode")
|
||||
await self.device.set_connectable(request.mode != NOT_CONNECTABLE)
|
||||
return empty_pb2.Empty()
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import grpc
|
||||
import logging
|
||||
|
||||
@@ -27,8 +28,8 @@ from bumble.core import (
|
||||
)
|
||||
from bumble.device import Connection as BumbleConnection, Device
|
||||
from bumble.hci import HCI_Error
|
||||
from bumble.utils import EventWatcher
|
||||
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
||||
from contextlib import suppress
|
||||
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
||||
from google.protobuf import wrappers_pb2 # pytype: disable=pyi-error
|
||||
@@ -99,7 +100,7 @@ class PairingDelegate(BasePairingDelegate):
|
||||
return ev
|
||||
|
||||
async def confirm(self, auto: bool = False) -> bool:
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
f"Pairing event: `just_works` (io_capability: {self.io_capability})"
|
||||
)
|
||||
|
||||
@@ -114,7 +115,7 @@ class PairingDelegate(BasePairingDelegate):
|
||||
return answer.confirm
|
||||
|
||||
async def compare_numbers(self, number: int, digits: int = 6) -> bool:
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
f"Pairing event: `numeric_comparison` (io_capability: {self.io_capability})"
|
||||
)
|
||||
|
||||
@@ -129,7 +130,7 @@ class PairingDelegate(BasePairingDelegate):
|
||||
return answer.confirm
|
||||
|
||||
async def get_number(self) -> Optional[int]:
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
f"Pairing event: `passkey_entry_request` (io_capability: {self.io_capability})"
|
||||
)
|
||||
|
||||
@@ -146,7 +147,7 @@ class PairingDelegate(BasePairingDelegate):
|
||||
return answer.passkey
|
||||
|
||||
async def get_string(self, max_length: int) -> Optional[str]:
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
f"Pairing event: `pin_code_request` (io_capability: {self.io_capability})"
|
||||
)
|
||||
|
||||
@@ -177,7 +178,7 @@ class PairingDelegate(BasePairingDelegate):
|
||||
):
|
||||
return
|
||||
|
||||
self.log.info(
|
||||
self.log.debug(
|
||||
f"Pairing event: `passkey_entry_notification` (io_capability: {self.io_capability})"
|
||||
)
|
||||
|
||||
@@ -232,6 +233,11 @@ class SecurityService(SecurityServicer):
|
||||
sc=config.pairing_sc_enable,
|
||||
mitm=config.pairing_mitm_enable,
|
||||
bonding=config.pairing_bonding_enable,
|
||||
identity_address_type=(
|
||||
PairingConfig.AddressType.PUBLIC
|
||||
if connection.self_address.is_public
|
||||
else config.identity_address_type
|
||||
),
|
||||
delegate=PairingDelegate(
|
||||
connection,
|
||||
self,
|
||||
@@ -247,7 +253,7 @@ class SecurityService(SecurityServicer):
|
||||
async def OnPairing(
|
||||
self, request: AsyncIterator[PairingEventAnswer], context: grpc.ServicerContext
|
||||
) -> AsyncGenerator[PairingEvent, None]:
|
||||
self.log.info('OnPairing')
|
||||
self.log.debug('OnPairing')
|
||||
|
||||
if self.event_queue is not None:
|
||||
raise RuntimeError('already streaming pairing events')
|
||||
@@ -273,7 +279,7 @@ class SecurityService(SecurityServicer):
|
||||
self, request: SecureRequest, context: grpc.ServicerContext
|
||||
) -> SecureResponse:
|
||||
connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
|
||||
self.log.info(f"Secure: {connection_handle}")
|
||||
self.log.debug(f"Secure: {connection_handle}")
|
||||
|
||||
connection = self.device.lookup_connection(connection_handle)
|
||||
assert connection
|
||||
@@ -291,25 +297,37 @@ class SecurityService(SecurityServicer):
|
||||
# trigger pairing if needed
|
||||
if self.need_pairing(connection, level):
|
||||
try:
|
||||
self.log.info('Pair...')
|
||||
self.log.debug('Pair...')
|
||||
|
||||
if (
|
||||
connection.transport == BT_LE_TRANSPORT
|
||||
and connection.role == BT_PERIPHERAL_ROLE
|
||||
):
|
||||
wait_for_security: asyncio.Future[
|
||||
bool
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
connection.on("pairing", lambda *_: wait_for_security.set_result(True)) # type: ignore
|
||||
connection.on("pairing_failure", wait_for_security.set_exception)
|
||||
security_result = asyncio.get_running_loop().create_future()
|
||||
|
||||
connection.request_pairing()
|
||||
with contextlib.closing(EventWatcher()) as watcher:
|
||||
|
||||
await wait_for_security
|
||||
else:
|
||||
await connection.pair()
|
||||
@watcher.on(connection, 'pairing')
|
||||
def on_pairing(*_: Any) -> None:
|
||||
security_result.set_result('success')
|
||||
|
||||
self.log.info('Paired')
|
||||
@watcher.on(connection, 'pairing_failure')
|
||||
def on_pairing_failure(*_: Any) -> None:
|
||||
security_result.set_result('pairing_failure')
|
||||
|
||||
@watcher.on(connection, 'disconnection')
|
||||
def on_disconnection(*_: Any) -> None:
|
||||
security_result.set_result('connection_died')
|
||||
|
||||
if (
|
||||
connection.transport == BT_LE_TRANSPORT
|
||||
and connection.role == BT_PERIPHERAL_ROLE
|
||||
):
|
||||
connection.request_pairing()
|
||||
else:
|
||||
await connection.pair()
|
||||
|
||||
result = await security_result
|
||||
|
||||
self.log.debug(f'Pairing session complete, status={result}')
|
||||
if result != 'success':
|
||||
return SecureResponse(**{result: empty_pb2.Empty()})
|
||||
except asyncio.CancelledError:
|
||||
self.log.warning("Connection died during encryption")
|
||||
return SecureResponse(connection_died=empty_pb2.Empty())
|
||||
@@ -320,9 +338,9 @@ class SecurityService(SecurityServicer):
|
||||
# trigger authentication if needed
|
||||
if self.need_authentication(connection, level):
|
||||
try:
|
||||
self.log.info('Authenticate...')
|
||||
self.log.debug('Authenticate...')
|
||||
await connection.authenticate()
|
||||
self.log.info('Authenticated')
|
||||
self.log.debug('Authenticated')
|
||||
except asyncio.CancelledError:
|
||||
self.log.warning("Connection died during authentication")
|
||||
return SecureResponse(connection_died=empty_pb2.Empty())
|
||||
@@ -333,9 +351,9 @@ class SecurityService(SecurityServicer):
|
||||
# trigger encryption if needed
|
||||
if self.need_encryption(connection, level):
|
||||
try:
|
||||
self.log.info('Encrypt...')
|
||||
self.log.debug('Encrypt...')
|
||||
await connection.encrypt()
|
||||
self.log.info('Encrypted')
|
||||
self.log.debug('Encrypted')
|
||||
except asyncio.CancelledError:
|
||||
self.log.warning("Connection died during encryption")
|
||||
return SecureResponse(connection_died=empty_pb2.Empty())
|
||||
@@ -353,7 +371,7 @@ class SecurityService(SecurityServicer):
|
||||
self, request: WaitSecurityRequest, context: grpc.ServicerContext
|
||||
) -> WaitSecurityResponse:
|
||||
connection_handle = int.from_bytes(request.connection.cookie.value, 'big')
|
||||
self.log.info(f"WaitSecurity: {connection_handle}")
|
||||
self.log.debug(f"WaitSecurity: {connection_handle}")
|
||||
|
||||
connection = self.device.lookup_connection(connection_handle)
|
||||
assert connection
|
||||
@@ -368,6 +386,7 @@ class SecurityService(SecurityServicer):
|
||||
str
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
authenticate_task: Optional[asyncio.Future[None]] = None
|
||||
pair_task: Optional[asyncio.Future[None]] = None
|
||||
|
||||
async def authenticate() -> None:
|
||||
assert connection
|
||||
@@ -390,7 +409,7 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
def set_failure(name: str) -> Callable[..., None]:
|
||||
def wrapper(*args: Any) -> None:
|
||||
self.log.info(f'Wait for security: error `{name}`: {args}')
|
||||
self.log.debug(f'Wait for security: error `{name}`: {args}')
|
||||
wait_for_security.set_result(name)
|
||||
|
||||
return wrapper
|
||||
@@ -398,13 +417,13 @@ class SecurityService(SecurityServicer):
|
||||
def try_set_success(*_: Any) -> None:
|
||||
assert connection
|
||||
if self.reached_security_level(connection, level):
|
||||
self.log.info('Wait for security: done')
|
||||
self.log.debug('Wait for security: done')
|
||||
wait_for_security.set_result('success')
|
||||
|
||||
def on_encryption_change(*_: Any) -> None:
|
||||
assert connection
|
||||
if self.reached_security_level(connection, level):
|
||||
self.log.info('Wait for security: done')
|
||||
self.log.debug('Wait for security: done')
|
||||
wait_for_security.set_result('success')
|
||||
elif (
|
||||
connection.transport == BT_BR_EDR_TRANSPORT
|
||||
@@ -414,6 +433,10 @@ class SecurityService(SecurityServicer):
|
||||
if authenticate_task is None:
|
||||
authenticate_task = asyncio.create_task(authenticate())
|
||||
|
||||
def pair(*_: Any) -> None:
|
||||
if self.need_pairing(connection, level):
|
||||
pair_task = asyncio.create_task(connection.pair())
|
||||
|
||||
listeners: Dict[str, Callable[..., None]] = {
|
||||
'disconnection': set_failure('connection_died'),
|
||||
'pairing_failure': set_failure('pairing_failure'),
|
||||
@@ -422,6 +445,9 @@ class SecurityService(SecurityServicer):
|
||||
'pairing': try_set_success,
|
||||
'connection_authentication': try_set_success,
|
||||
'connection_encryption_change': on_encryption_change,
|
||||
'classic_pairing': try_set_success,
|
||||
'classic_pairing_failure': set_failure('pairing_failure'),
|
||||
'security_request': pair,
|
||||
}
|
||||
|
||||
# register event handlers
|
||||
@@ -432,7 +458,7 @@ class SecurityService(SecurityServicer):
|
||||
if self.reached_security_level(connection, level):
|
||||
return WaitSecurityResponse(success=empty_pb2.Empty())
|
||||
|
||||
self.log.info('Wait for security...')
|
||||
self.log.debug('Wait for security...')
|
||||
kwargs = {}
|
||||
kwargs[await wait_for_security] = empty_pb2.Empty()
|
||||
|
||||
@@ -442,12 +468,21 @@ class SecurityService(SecurityServicer):
|
||||
|
||||
# wait for `authenticate` to finish if any
|
||||
if authenticate_task is not None:
|
||||
self.log.info('Wait for authentication...')
|
||||
self.log.debug('Wait for authentication...')
|
||||
try:
|
||||
await authenticate_task # type: ignore
|
||||
except:
|
||||
pass
|
||||
self.log.info('Authenticated')
|
||||
self.log.debug('Authenticated')
|
||||
|
||||
# wait for `pair` to finish if any
|
||||
if pair_task is not None:
|
||||
self.log.debug('Wait for authentication...')
|
||||
try:
|
||||
await pair_task # type: ignore
|
||||
except:
|
||||
pass
|
||||
self.log.debug('paired')
|
||||
|
||||
return WaitSecurityResponse(**kwargs)
|
||||
|
||||
@@ -503,7 +538,7 @@ class SecurityStorageService(SecurityStorageServicer):
|
||||
self, request: IsBondedRequest, context: grpc.ServicerContext
|
||||
) -> wrappers_pb2.BoolValue:
|
||||
address = utils.address_from_request(request, request.WhichOneof("address"))
|
||||
self.log.info(f"IsBonded: {address}")
|
||||
self.log.debug(f"IsBonded: {address}")
|
||||
|
||||
if self.device.keystore is not None:
|
||||
is_bonded = await self.device.keystore.get(str(address)) is not None
|
||||
@@ -517,10 +552,10 @@ class SecurityStorageService(SecurityStorageServicer):
|
||||
self, request: DeleteBondRequest, context: grpc.ServicerContext
|
||||
) -> empty_pb2.Empty:
|
||||
address = utils.address_from_request(request, request.WhichOneof("address"))
|
||||
self.log.info(f"DeleteBond: {address}")
|
||||
self.log.debug(f"DeleteBond: {address}")
|
||||
|
||||
if self.device.keystore is not None:
|
||||
with suppress(KeyError):
|
||||
with contextlib.suppress(KeyError):
|
||||
await self.device.keystore.delete(str(address))
|
||||
|
||||
return empty_pb2.Empty()
|
||||
|
||||
295
bumble/rfcomm.py
295
bumble/rfcomm.py
@@ -15,15 +15,37 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import enum
|
||||
from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
||||
|
||||
from pyee import EventEmitter
|
||||
from typing import Optional, Tuple, Callable, Dict, Union
|
||||
|
||||
from . import core, l2cap
|
||||
from .colors import color
|
||||
from .core import BT_BR_EDR_TRANSPORT, InvalidStateError, ProtocolError
|
||||
from .core import (
|
||||
UUID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
InvalidStateError,
|
||||
ProtocolError,
|
||||
)
|
||||
from .sdp import (
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bumble.device import Device, Connection
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -105,6 +127,50 @@ RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
|
||||
# fmt: on
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_service_sdp_records(
|
||||
service_record_handle: int, channel: int, uuid: Optional[UUID] = None
|
||||
) -> List[ServiceAttribute]:
|
||||
"""
|
||||
Create SDP records for an RFComm service given a channel number and an
|
||||
optional UUID. A Service Class Attribute is included only if the UUID is not None.
|
||||
"""
|
||||
records = [
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(service_record_handle),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence([DataElement.uuid(BT_L2CAP_PROTOCOL_ID)]),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_8(channel),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
if uuid:
|
||||
records.append(
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(uuid)]),
|
||||
)
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def compute_fcs(buffer: bytes) -> int:
|
||||
result = 0xFF
|
||||
@@ -149,9 +215,9 @@ class RFCOMM_Frame:
|
||||
return RFCOMM_FRAME_TYPE_NAMES[self.type]
|
||||
|
||||
@staticmethod
|
||||
def parse_mcc(data) -> Tuple[int, int, bytes]:
|
||||
def parse_mcc(data) -> Tuple[int, bool, bytes]:
|
||||
mcc_type = data[0] >> 2
|
||||
c_r = (data[0] >> 1) & 1
|
||||
c_r = bool((data[0] >> 1) & 1)
|
||||
length = data[1]
|
||||
if data[1] & 1:
|
||||
length >>= 1
|
||||
@@ -192,7 +258,7 @@ class RFCOMM_Frame:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes):
|
||||
def from_bytes(data: bytes) -> RFCOMM_Frame:
|
||||
# Extract fields
|
||||
dlci = (data[0] >> 2) & 0x3F
|
||||
c_r = (data[0] >> 1) & 0x01
|
||||
@@ -215,7 +281,7 @@ class RFCOMM_Frame:
|
||||
|
||||
return frame
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
return (
|
||||
bytes([self.address, self.control])
|
||||
+ self.length
|
||||
@@ -223,7 +289,7 @@ class RFCOMM_Frame:
|
||||
+ bytes([self.fcs])
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'{color(self.type_name(), "yellow")}'
|
||||
f'(c/r={self.c_r},'
|
||||
@@ -253,7 +319,7 @@ class RFCOMM_MCC_PN:
|
||||
max_frame_size: int,
|
||||
max_retransmissions: int,
|
||||
window_size: int,
|
||||
):
|
||||
) -> None:
|
||||
self.dlci = dlci
|
||||
self.cl = cl
|
||||
self.priority = priority
|
||||
@@ -263,7 +329,7 @@ class RFCOMM_MCC_PN:
|
||||
self.window_size = window_size
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes):
|
||||
def from_bytes(data: bytes) -> RFCOMM_MCC_PN:
|
||||
return RFCOMM_MCC_PN(
|
||||
dlci=data[0],
|
||||
cl=data[1],
|
||||
@@ -274,7 +340,7 @@ class RFCOMM_MCC_PN:
|
||||
window_size=data[7],
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(
|
||||
[
|
||||
self.dlci & 0xFF,
|
||||
@@ -288,7 +354,7 @@ class RFCOMM_MCC_PN:
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'PN(dlci={self.dlci},'
|
||||
f'cl={self.cl},'
|
||||
@@ -309,7 +375,9 @@ class RFCOMM_MCC_MSC:
|
||||
ic: int
|
||||
dv: int
|
||||
|
||||
def __init__(self, dlci: int, fc: int, rtc: int, rtr: int, ic: int, dv: int):
|
||||
def __init__(
|
||||
self, dlci: int, fc: int, rtc: int, rtr: int, ic: int, dv: int
|
||||
) -> None:
|
||||
self.dlci = dlci
|
||||
self.fc = fc
|
||||
self.rtc = rtc
|
||||
@@ -318,7 +386,7 @@ class RFCOMM_MCC_MSC:
|
||||
self.dv = dv
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes):
|
||||
def from_bytes(data: bytes) -> RFCOMM_MCC_MSC:
|
||||
return RFCOMM_MCC_MSC(
|
||||
dlci=data[0] >> 2,
|
||||
fc=data[1] >> 1 & 1,
|
||||
@@ -328,7 +396,7 @@ class RFCOMM_MCC_MSC:
|
||||
dv=data[1] >> 7 & 1,
|
||||
)
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(
|
||||
[
|
||||
(self.dlci << 2) | 3,
|
||||
@@ -341,7 +409,7 @@ class RFCOMM_MCC_MSC:
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'MSC(dlci={self.dlci},'
|
||||
f'fc={self.fc},'
|
||||
@@ -354,29 +422,24 @@ class RFCOMM_MCC_MSC:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class DLC(EventEmitter):
|
||||
# States
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
DISCONNECTING = 0x03
|
||||
DISCONNECTED = 0x04
|
||||
RESET = 0x05
|
||||
|
||||
STATE_NAMES = {
|
||||
INIT: 'INIT',
|
||||
CONNECTING: 'CONNECTING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
DISCONNECTING: 'DISCONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
RESET: 'RESET',
|
||||
}
|
||||
class State(enum.IntEnum):
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
DISCONNECTING = 0x03
|
||||
DISCONNECTED = 0x04
|
||||
RESET = 0x05
|
||||
|
||||
connection_result: Optional[asyncio.Future]
|
||||
sink: Optional[Callable[[bytes], None]]
|
||||
|
||||
def __init__(
|
||||
self, multiplexer, dlci: int, max_frame_size: int, initial_tx_credits: int
|
||||
):
|
||||
self,
|
||||
multiplexer: Multiplexer,
|
||||
dlci: int,
|
||||
max_frame_size: int,
|
||||
initial_tx_credits: int,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.multiplexer = multiplexer
|
||||
self.dlci = dlci
|
||||
@@ -384,9 +447,9 @@ class DLC(EventEmitter):
|
||||
self.rx_threshold = self.rx_credits // 2
|
||||
self.tx_credits = initial_tx_credits
|
||||
self.tx_buffer = b''
|
||||
self.state = DLC.INIT
|
||||
self.state = DLC.State.INIT
|
||||
self.role = multiplexer.role
|
||||
self.c_r = 1 if self.role == Multiplexer.INITIATOR else 0
|
||||
self.c_r = 1 if self.role == Multiplexer.Role.INITIATOR else 0
|
||||
self.sink = None
|
||||
self.connection_result = None
|
||||
|
||||
@@ -396,14 +459,8 @@ class DLC(EventEmitter):
|
||||
max_frame_size, self.multiplexer.l2cap_channel.mtu - max_overhead
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def state_name(state: int) -> str:
|
||||
return DLC.STATE_NAMES[state]
|
||||
|
||||
def change_state(self, new_state: int) -> None:
|
||||
logger.debug(
|
||||
f'{self} state change -> {color(self.state_name(new_state), "magenta")}'
|
||||
)
|
||||
def change_state(self, new_state: State) -> None:
|
||||
logger.debug(f'{self} state change -> {color(new_state.name, "magenta")}')
|
||||
self.state = new_state
|
||||
|
||||
def send_frame(self, frame: RFCOMM_Frame) -> None:
|
||||
@@ -413,8 +470,8 @@ class DLC(EventEmitter):
|
||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||
handler(frame)
|
||||
|
||||
def on_sabm_frame(self, _frame) -> None:
|
||||
if self.state != DLC.CONNECTING:
|
||||
def on_sabm_frame(self, _frame: RFCOMM_Frame) -> None:
|
||||
if self.state != DLC.State.CONNECTING:
|
||||
logger.warning(
|
||||
color('!!! received SABM when not in CONNECTING state', 'red')
|
||||
)
|
||||
@@ -430,11 +487,11 @@ class DLC(EventEmitter):
|
||||
logger.debug(f'>>> MCC MSC Command: {msc}')
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
|
||||
self.change_state(DLC.CONNECTED)
|
||||
self.change_state(DLC.State.CONNECTED)
|
||||
self.emit('open')
|
||||
|
||||
def on_ua_frame(self, _frame) -> None:
|
||||
if self.state != DLC.CONNECTING:
|
||||
def on_ua_frame(self, _frame: RFCOMM_Frame) -> None:
|
||||
if self.state != DLC.State.CONNECTING:
|
||||
logger.warning(
|
||||
color('!!! received SABM when not in CONNECTING state', 'red')
|
||||
)
|
||||
@@ -448,14 +505,14 @@ class DLC(EventEmitter):
|
||||
logger.debug(f'>>> MCC MSC Command: {msc}')
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
|
||||
self.change_state(DLC.CONNECTED)
|
||||
self.change_state(DLC.State.CONNECTED)
|
||||
self.multiplexer.on_dlc_open_complete(self)
|
||||
|
||||
def on_dm_frame(self, frame) -> None:
|
||||
def on_dm_frame(self, frame: RFCOMM_Frame) -> None:
|
||||
# TODO: handle all states
|
||||
pass
|
||||
|
||||
def on_disc_frame(self, _frame) -> None:
|
||||
def on_disc_frame(self, _frame: RFCOMM_Frame) -> None:
|
||||
# TODO: handle all states
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r=1 - self.c_r, dlci=self.dlci))
|
||||
|
||||
@@ -489,10 +546,10 @@ class DLC(EventEmitter):
|
||||
# Check if there's anything to send (including credits)
|
||||
self.process_tx()
|
||||
|
||||
def on_ui_frame(self, frame) -> None:
|
||||
def on_ui_frame(self, frame: RFCOMM_Frame) -> None:
|
||||
pass
|
||||
|
||||
def on_mcc_msc(self, c_r, msc) -> None:
|
||||
def on_mcc_msc(self, c_r: bool, msc: RFCOMM_MCC_MSC) -> None:
|
||||
if c_r:
|
||||
# Command
|
||||
logger.debug(f'<<< MCC MSC Command: {msc}')
|
||||
@@ -507,15 +564,15 @@ class DLC(EventEmitter):
|
||||
logger.debug(f'<<< MCC MSC Response: {msc}')
|
||||
|
||||
def connect(self) -> None:
|
||||
if self.state != DLC.INIT:
|
||||
if self.state != DLC.State.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
self.change_state(DLC.CONNECTING)
|
||||
self.change_state(DLC.State.CONNECTING)
|
||||
self.connection_result = asyncio.get_running_loop().create_future()
|
||||
self.send_frame(RFCOMM_Frame.sabm(c_r=self.c_r, dlci=self.dlci))
|
||||
|
||||
def accept(self) -> None:
|
||||
if self.state != DLC.INIT:
|
||||
if self.state != DLC.State.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
pn = RFCOMM_MCC_PN(
|
||||
@@ -530,7 +587,7 @@ class DLC(EventEmitter):
|
||||
mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=0, data=bytes(pn))
|
||||
logger.debug(f'>>> PN Response: {pn}')
|
||||
self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
|
||||
self.change_state(DLC.CONNECTING)
|
||||
self.change_state(DLC.State.CONNECTING)
|
||||
|
||||
def rx_credits_needed(self) -> int:
|
||||
if self.rx_credits <= self.rx_threshold:
|
||||
@@ -592,34 +649,24 @@ class DLC(EventEmitter):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return f'DLC(dlci={self.dlci},state={self.state_name(self.state)})'
|
||||
def __str__(self) -> str:
|
||||
return f'DLC(dlci={self.dlci},state={self.state.name})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Multiplexer(EventEmitter):
|
||||
# Roles
|
||||
INITIATOR = 0x00
|
||||
RESPONDER = 0x01
|
||||
class Role(enum.IntEnum):
|
||||
INITIATOR = 0x00
|
||||
RESPONDER = 0x01
|
||||
|
||||
# States
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
OPENING = 0x03
|
||||
DISCONNECTING = 0x04
|
||||
DISCONNECTED = 0x05
|
||||
RESET = 0x06
|
||||
|
||||
STATE_NAMES = {
|
||||
INIT: 'INIT',
|
||||
CONNECTING: 'CONNECTING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
OPENING: 'OPENING',
|
||||
DISCONNECTING: 'DISCONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
RESET: 'RESET',
|
||||
}
|
||||
class State(enum.IntEnum):
|
||||
INIT = 0x00
|
||||
CONNECTING = 0x01
|
||||
CONNECTED = 0x02
|
||||
OPENING = 0x03
|
||||
DISCONNECTING = 0x04
|
||||
DISCONNECTED = 0x05
|
||||
RESET = 0x06
|
||||
|
||||
connection_result: Optional[asyncio.Future]
|
||||
disconnection_result: Optional[asyncio.Future]
|
||||
@@ -627,11 +674,11 @@ class Multiplexer(EventEmitter):
|
||||
acceptor: Optional[Callable[[int], bool]]
|
||||
dlcs: Dict[int, DLC]
|
||||
|
||||
def __init__(self, l2cap_channel: l2cap.Channel, role: int) -> None:
|
||||
def __init__(self, l2cap_channel: l2cap.Channel, role: Role) -> None:
|
||||
super().__init__()
|
||||
self.role = role
|
||||
self.l2cap_channel = l2cap_channel
|
||||
self.state = Multiplexer.INIT
|
||||
self.state = Multiplexer.State.INIT
|
||||
self.dlcs = {} # DLCs, by DLCI
|
||||
self.connection_result = None
|
||||
self.disconnection_result = None
|
||||
@@ -641,14 +688,8 @@ class Multiplexer(EventEmitter):
|
||||
# Become a sink for the L2CAP channel
|
||||
l2cap_channel.sink = self.on_pdu
|
||||
|
||||
@staticmethod
|
||||
def state_name(state: int):
|
||||
return Multiplexer.STATE_NAMES[state]
|
||||
|
||||
def change_state(self, new_state: int) -> None:
|
||||
logger.debug(
|
||||
f'{self} state change -> {color(self.state_name(new_state), "cyan")}'
|
||||
)
|
||||
def change_state(self, new_state: State) -> None:
|
||||
logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
|
||||
self.state = new_state
|
||||
|
||||
def send_frame(self, frame: RFCOMM_Frame) -> None:
|
||||
@@ -679,28 +720,28 @@ class Multiplexer(EventEmitter):
|
||||
handler = getattr(self, f'on_{frame.type_name()}_frame'.lower())
|
||||
handler(frame)
|
||||
|
||||
def on_sabm_frame(self, _frame) -> None:
|
||||
if self.state != Multiplexer.INIT:
|
||||
def on_sabm_frame(self, _frame: RFCOMM_Frame) -> None:
|
||||
if self.state != Multiplexer.State.INIT:
|
||||
logger.debug('not in INIT state, ignoring SABM')
|
||||
return
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
self.change_state(Multiplexer.State.CONNECTED)
|
||||
self.send_frame(RFCOMM_Frame.ua(c_r=1, dlci=0))
|
||||
|
||||
def on_ua_frame(self, _frame) -> None:
|
||||
if self.state == Multiplexer.CONNECTING:
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
def on_ua_frame(self, _frame: RFCOMM_Frame) -> None:
|
||||
if self.state == Multiplexer.State.CONNECTING:
|
||||
self.change_state(Multiplexer.State.CONNECTED)
|
||||
if self.connection_result:
|
||||
self.connection_result.set_result(0)
|
||||
self.connection_result = None
|
||||
elif self.state == Multiplexer.DISCONNECTING:
|
||||
self.change_state(Multiplexer.DISCONNECTED)
|
||||
elif self.state == Multiplexer.State.DISCONNECTING:
|
||||
self.change_state(Multiplexer.State.DISCONNECTED)
|
||||
if self.disconnection_result:
|
||||
self.disconnection_result.set_result(None)
|
||||
self.disconnection_result = None
|
||||
|
||||
def on_dm_frame(self, _frame) -> None:
|
||||
if self.state == Multiplexer.OPENING:
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
def on_dm_frame(self, _frame: RFCOMM_Frame) -> None:
|
||||
if self.state == Multiplexer.State.OPENING:
|
||||
self.change_state(Multiplexer.State.CONNECTED)
|
||||
if self.open_result:
|
||||
self.open_result.set_exception(
|
||||
core.ConnectionError(
|
||||
@@ -713,10 +754,12 @@ class Multiplexer(EventEmitter):
|
||||
else:
|
||||
logger.warning(f'unexpected state for DM: {self}')
|
||||
|
||||
def on_disc_frame(self, _frame) -> None:
|
||||
self.change_state(Multiplexer.DISCONNECTED)
|
||||
def on_disc_frame(self, _frame: RFCOMM_Frame) -> None:
|
||||
self.change_state(Multiplexer.State.DISCONNECTED)
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.ua(c_r=0 if self.role == Multiplexer.INITIATOR else 1, dlci=0)
|
||||
RFCOMM_Frame.ua(
|
||||
c_r=0 if self.role == Multiplexer.Role.INITIATOR else 1, dlci=0
|
||||
)
|
||||
)
|
||||
|
||||
def on_uih_frame(self, frame: RFCOMM_Frame) -> None:
|
||||
@@ -729,11 +772,11 @@ class Multiplexer(EventEmitter):
|
||||
mcs = RFCOMM_MCC_MSC.from_bytes(value)
|
||||
self.on_mcc_msc(c_r, mcs)
|
||||
|
||||
def on_ui_frame(self, frame) -> None:
|
||||
def on_ui_frame(self, frame: RFCOMM_Frame) -> None:
|
||||
pass
|
||||
|
||||
def on_mcc_pn(self, c_r, pn) -> None:
|
||||
if c_r == 1:
|
||||
def on_mcc_pn(self, c_r: bool, pn: RFCOMM_MCC_PN) -> None:
|
||||
if c_r:
|
||||
# Command
|
||||
logger.debug(f'<<< PN Command: {pn}')
|
||||
|
||||
@@ -764,14 +807,14 @@ class Multiplexer(EventEmitter):
|
||||
else:
|
||||
# Response
|
||||
logger.debug(f'>>> PN Response: {pn}')
|
||||
if self.state == Multiplexer.OPENING:
|
||||
if self.state == Multiplexer.State.OPENING:
|
||||
dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size)
|
||||
self.dlcs[pn.dlci] = dlc
|
||||
dlc.connect()
|
||||
else:
|
||||
logger.warning('ignoring PN response')
|
||||
|
||||
def on_mcc_msc(self, c_r, msc) -> None:
|
||||
def on_mcc_msc(self, c_r: bool, msc: RFCOMM_MCC_MSC) -> None:
|
||||
dlc = self.dlcs.get(msc.dlci)
|
||||
if dlc is None:
|
||||
logger.warning(f'no dlc for DLCI {msc.dlci}')
|
||||
@@ -779,30 +822,30 @@ class Multiplexer(EventEmitter):
|
||||
dlc.on_mcc_msc(c_r, msc)
|
||||
|
||||
async def connect(self) -> None:
|
||||
if self.state != Multiplexer.INIT:
|
||||
if self.state != Multiplexer.State.INIT:
|
||||
raise InvalidStateError('invalid state')
|
||||
|
||||
self.change_state(Multiplexer.CONNECTING)
|
||||
self.change_state(Multiplexer.State.CONNECTING)
|
||||
self.connection_result = asyncio.get_running_loop().create_future()
|
||||
self.send_frame(RFCOMM_Frame.sabm(c_r=1, dlci=0))
|
||||
return await self.connection_result
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
if self.state != Multiplexer.CONNECTED:
|
||||
if self.state != Multiplexer.State.CONNECTED:
|
||||
return
|
||||
|
||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||
self.change_state(Multiplexer.DISCONNECTING)
|
||||
self.change_state(Multiplexer.State.DISCONNECTING)
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.disc(
|
||||
c_r=1 if self.role == Multiplexer.INITIATOR else 0, dlci=0
|
||||
c_r=1 if self.role == Multiplexer.Role.INITIATOR else 0, dlci=0
|
||||
)
|
||||
)
|
||||
await self.disconnection_result
|
||||
|
||||
async def open_dlc(self, channel: int) -> DLC:
|
||||
if self.state != Multiplexer.CONNECTED:
|
||||
if self.state == Multiplexer.OPENING:
|
||||
if self.state != Multiplexer.State.CONNECTED:
|
||||
if self.state == Multiplexer.State.OPENING:
|
||||
raise InvalidStateError('open already in progress')
|
||||
|
||||
raise InvalidStateError('not connected')
|
||||
@@ -819,10 +862,10 @@ class Multiplexer(EventEmitter):
|
||||
mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=1, data=bytes(pn))
|
||||
logger.debug(f'>>> Sending MCC: {pn}')
|
||||
self.open_result = asyncio.get_running_loop().create_future()
|
||||
self.change_state(Multiplexer.OPENING)
|
||||
self.change_state(Multiplexer.State.OPENING)
|
||||
self.send_frame(
|
||||
RFCOMM_Frame.uih(
|
||||
c_r=1 if self.role == Multiplexer.INITIATOR else 0,
|
||||
c_r=1 if self.role == Multiplexer.Role.INITIATOR else 0,
|
||||
dlci=0,
|
||||
information=mcc,
|
||||
)
|
||||
@@ -831,14 +874,14 @@ class Multiplexer(EventEmitter):
|
||||
self.open_result = None
|
||||
return result
|
||||
|
||||
def on_dlc_open_complete(self, dlc: DLC):
|
||||
def on_dlc_open_complete(self, dlc: DLC) -> None:
|
||||
logger.debug(f'DLC [{dlc.dlci}] open complete')
|
||||
self.change_state(Multiplexer.CONNECTED)
|
||||
self.change_state(Multiplexer.State.CONNECTED)
|
||||
if self.open_result:
|
||||
self.open_result.set_result(dlc)
|
||||
|
||||
def __str__(self):
|
||||
return f'Multiplexer(state={self.state_name(self.state)})'
|
||||
def __str__(self) -> str:
|
||||
return f'Multiplexer(state={self.state.name})'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -846,7 +889,7 @@ class Client:
|
||||
multiplexer: Optional[Multiplexer]
|
||||
l2cap_channel: Optional[l2cap.Channel]
|
||||
|
||||
def __init__(self, device, connection) -> None:
|
||||
def __init__(self, device: Device, connection: Connection) -> None:
|
||||
self.device = device
|
||||
self.connection = connection
|
||||
self.l2cap_channel = None
|
||||
@@ -864,7 +907,7 @@ class Client:
|
||||
|
||||
assert self.l2cap_channel is not None
|
||||
# Create a mutliplexer to manage DLCs with the server
|
||||
self.multiplexer = Multiplexer(self.l2cap_channel, Multiplexer.INITIATOR)
|
||||
self.multiplexer = Multiplexer(self.l2cap_channel, Multiplexer.Role.INITIATOR)
|
||||
|
||||
# Connect the multiplexer
|
||||
await self.multiplexer.connect()
|
||||
@@ -886,7 +929,7 @@ class Client:
|
||||
class Server(EventEmitter):
|
||||
acceptors: Dict[int, Callable[[DLC], None]]
|
||||
|
||||
def __init__(self, device) -> None:
|
||||
def __init__(self, device: Device) -> None:
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.multiplexer = None
|
||||
@@ -925,7 +968,7 @@ class Server(EventEmitter):
|
||||
logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}')
|
||||
|
||||
# Create a new multiplexer for the channel
|
||||
multiplexer = Multiplexer(l2cap_channel, Multiplexer.RESPONDER)
|
||||
multiplexer = Multiplexer(l2cap_channel, Multiplexer.Role.RESPONDER)
|
||||
multiplexer.acceptor = self.accept_dlc
|
||||
multiplexer.on('dlc', self.on_dlc)
|
||||
|
||||
|
||||
115
bumble/sdp.py
115
bumble/sdp.py
@@ -18,13 +18,16 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import struct
|
||||
from typing import Dict, List, Type
|
||||
from typing import Dict, List, Type, Optional, Tuple, Union, NewType, TYPE_CHECKING
|
||||
|
||||
from . import core
|
||||
from . import core, l2cap
|
||||
from .colors import color
|
||||
from .core import InvalidStateError
|
||||
from .hci import HCI_Object, name_or_number, key_with_value
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import Device, Connection
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -94,6 +97,10 @@ SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID = 0X000B
|
||||
SDP_ICON_URL_ATTRIBUTE_ID = 0X000C
|
||||
SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X000D
|
||||
|
||||
# Attribute Identifier (cf. Assigned Numbers for Service Discovery)
|
||||
# used by AVRCP, HFP and A2DP
|
||||
SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID = 0x0311
|
||||
|
||||
SDP_ATTRIBUTE_ID_NAMES = {
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID: 'SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID',
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID: 'SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID',
|
||||
@@ -462,7 +469,7 @@ class ServiceAttribute:
|
||||
self.value = value
|
||||
|
||||
@staticmethod
|
||||
def list_from_data_elements(elements):
|
||||
def list_from_data_elements(elements: List[DataElement]) -> List[ServiceAttribute]:
|
||||
attribute_list = []
|
||||
for i in range(0, len(elements) // 2):
|
||||
attribute_id, attribute_value = elements[2 * i : 2 * (i + 1)]
|
||||
@@ -474,7 +481,9 @@ class ServiceAttribute:
|
||||
return attribute_list
|
||||
|
||||
@staticmethod
|
||||
def find_attribute_in_list(attribute_list, attribute_id):
|
||||
def find_attribute_in_list(
|
||||
attribute_list: List[ServiceAttribute], attribute_id: int
|
||||
) -> Optional[DataElement]:
|
||||
return next(
|
||||
(
|
||||
attribute.value
|
||||
@@ -489,7 +498,7 @@ class ServiceAttribute:
|
||||
return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id_code)
|
||||
|
||||
@staticmethod
|
||||
def is_uuid_in_value(uuid, value):
|
||||
def is_uuid_in_value(uuid: core.UUID, value: DataElement) -> bool:
|
||||
# Find if a uuid matches a value, either directly or recursing into sequences
|
||||
if value.type == DataElement.UUID:
|
||||
return value.value == uuid
|
||||
@@ -543,7 +552,9 @@ class SDP_PDU:
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def parse_service_record_handle_list_preceded_by_count(data, offset):
|
||||
def parse_service_record_handle_list_preceded_by_count(
|
||||
data: bytes, offset: int
|
||||
) -> Tuple[int, List[int]]:
|
||||
count = struct.unpack_from('>H', data, offset - 2)[0]
|
||||
handle_list = [
|
||||
struct.unpack_from('>I', data, offset + x * 4)[0] for x in range(count)
|
||||
@@ -641,6 +652,10 @@ class SDP_ServiceSearchRequest(SDP_PDU):
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.5.1 SDP_ServiceSearchRequest PDU
|
||||
'''
|
||||
|
||||
service_search_pattern: DataElement
|
||||
maximum_service_record_count: int
|
||||
continuation_state: bytes
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass(
|
||||
@@ -659,6 +674,11 @@ class SDP_ServiceSearchResponse(SDP_PDU):
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.5.2 SDP_ServiceSearchResponse PDU
|
||||
'''
|
||||
|
||||
service_record_handle_list: List[int]
|
||||
total_service_record_count: int
|
||||
current_service_record_count: int
|
||||
continuation_state: bytes
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass(
|
||||
@@ -674,6 +694,11 @@ class SDP_ServiceAttributeRequest(SDP_PDU):
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.6.1 SDP_ServiceAttributeRequest PDU
|
||||
'''
|
||||
|
||||
service_record_handle: int
|
||||
maximum_attribute_byte_count: int
|
||||
attribute_id_list: DataElement
|
||||
continuation_state: bytes
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass(
|
||||
@@ -688,6 +713,10 @@ class SDP_ServiceAttributeResponse(SDP_PDU):
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.6.2 SDP_ServiceAttributeResponse PDU
|
||||
'''
|
||||
|
||||
attribute_list_byte_count: int
|
||||
attribute_list: bytes
|
||||
continuation_state: bytes
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass(
|
||||
@@ -703,6 +732,11 @@ class SDP_ServiceSearchAttributeRequest(SDP_PDU):
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.7.1 SDP_ServiceSearchAttributeRequest PDU
|
||||
'''
|
||||
|
||||
service_search_pattern: DataElement
|
||||
maximum_attribute_byte_count: int
|
||||
attribute_id_list: DataElement
|
||||
continuation_state: bytes
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@SDP_PDU.subclass(
|
||||
@@ -717,26 +751,34 @@ class SDP_ServiceSearchAttributeResponse(SDP_PDU):
|
||||
See Bluetooth spec @ Vol 3, Part B - 4.7.2 SDP_ServiceSearchAttributeResponse PDU
|
||||
'''
|
||||
|
||||
attribute_list_byte_count: int
|
||||
attribute_list: bytes
|
||||
continuation_state: bytes
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Client:
|
||||
def __init__(self, device):
|
||||
channel: Optional[l2cap.Channel]
|
||||
|
||||
def __init__(self, device: Device) -> None:
|
||||
self.device = device
|
||||
self.pending_request = None
|
||||
self.channel = None
|
||||
|
||||
async def connect(self, connection):
|
||||
async def connect(self, connection: Connection) -> None:
|
||||
result = await self.device.l2cap_channel_manager.connect(connection, SDP_PSM)
|
||||
self.channel = result
|
||||
|
||||
async def disconnect(self):
|
||||
async def disconnect(self) -> None:
|
||||
if self.channel:
|
||||
await self.channel.disconnect()
|
||||
self.channel = None
|
||||
|
||||
async def search_services(self, uuids):
|
||||
async def search_services(self, uuids: List[core.UUID]) -> List[int]:
|
||||
if self.pending_request is not None:
|
||||
raise InvalidStateError('request already pending')
|
||||
if self.channel is None:
|
||||
raise InvalidStateError('L2CAP not connected')
|
||||
|
||||
service_search_pattern = DataElement.sequence(
|
||||
[DataElement.uuid(uuid) for uuid in uuids]
|
||||
@@ -766,9 +808,13 @@ class Client:
|
||||
|
||||
return service_record_handle_list
|
||||
|
||||
async def search_attributes(self, uuids, attribute_ids):
|
||||
async def search_attributes(
|
||||
self, uuids: List[core.UUID], attribute_ids: List[Union[int, Tuple[int, int]]]
|
||||
) -> List[List[ServiceAttribute]]:
|
||||
if self.pending_request is not None:
|
||||
raise InvalidStateError('request already pending')
|
||||
if self.channel is None:
|
||||
raise InvalidStateError('L2CAP not connected')
|
||||
|
||||
service_search_pattern = DataElement.sequence(
|
||||
[DataElement.uuid(uuid) for uuid in uuids]
|
||||
@@ -819,9 +865,15 @@ class Client:
|
||||
if sequence.type == DataElement.SEQUENCE
|
||||
]
|
||||
|
||||
async def get_attributes(self, service_record_handle, attribute_ids):
|
||||
async def get_attributes(
|
||||
self,
|
||||
service_record_handle: int,
|
||||
attribute_ids: List[Union[int, Tuple[int, int]]],
|
||||
) -> List[ServiceAttribute]:
|
||||
if self.pending_request is not None:
|
||||
raise InvalidStateError('request already pending')
|
||||
if self.channel is None:
|
||||
raise InvalidStateError('L2CAP not connected')
|
||||
|
||||
attribute_id_list = DataElement.sequence(
|
||||
[
|
||||
@@ -869,21 +921,25 @@ class Client:
|
||||
# -----------------------------------------------------------------------------
|
||||
class Server:
|
||||
CONTINUATION_STATE = bytes([0x01, 0x43])
|
||||
channel: Optional[l2cap.Channel]
|
||||
Service = NewType('Service', List[ServiceAttribute])
|
||||
service_records: Dict[int, Service]
|
||||
current_response: Union[None, bytes, Tuple[int, List[int]]]
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, device: Device) -> None:
|
||||
self.device = device
|
||||
self.service_records = {} # Service records maps, by record handle
|
||||
self.channel = None
|
||||
self.current_response = None
|
||||
|
||||
def register(self, l2cap_channel_manager):
|
||||
def register(self, l2cap_channel_manager: l2cap.ChannelManager) -> None:
|
||||
l2cap_channel_manager.register_server(SDP_PSM, self.on_connection)
|
||||
|
||||
def send_response(self, response):
|
||||
logger.debug(f'{color(">>> Sending SDP Response", "blue")}: {response}')
|
||||
self.channel.send_pdu(response)
|
||||
|
||||
def match_services(self, search_pattern):
|
||||
def match_services(self, search_pattern: DataElement) -> Dict[int, Service]:
|
||||
# Find the services for which the attributes in the pattern is a subset of the
|
||||
# service's attribute values (NOTE: the value search recurses into sequences)
|
||||
matching_services = {}
|
||||
@@ -953,7 +1009,9 @@ class Server:
|
||||
return (payload, continuation_state)
|
||||
|
||||
@staticmethod
|
||||
def get_service_attributes(service, attribute_ids):
|
||||
def get_service_attributes(
|
||||
service: Service, attribute_ids: List[DataElement]
|
||||
) -> DataElement:
|
||||
attributes = []
|
||||
for attribute_id in attribute_ids:
|
||||
if attribute_id.value_size == 4:
|
||||
@@ -978,10 +1036,10 @@ class Server:
|
||||
|
||||
return attribute_list
|
||||
|
||||
def on_sdp_service_search_request(self, request):
|
||||
def on_sdp_service_search_request(self, request: SDP_ServiceSearchRequest) -> None:
|
||||
# Check if this is a continuation
|
||||
if len(request.continuation_state) > 1:
|
||||
if not self.current_response:
|
||||
if self.current_response is None:
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id=request.transaction_id,
|
||||
@@ -1010,6 +1068,7 @@ class Server:
|
||||
)
|
||||
|
||||
# Respond, keeping any unsent handles for later
|
||||
assert isinstance(self.current_response, tuple)
|
||||
service_record_handles = self.current_response[1][
|
||||
: request.maximum_service_record_count
|
||||
]
|
||||
@@ -1033,10 +1092,12 @@ class Server:
|
||||
)
|
||||
)
|
||||
|
||||
def on_sdp_service_attribute_request(self, request):
|
||||
def on_sdp_service_attribute_request(
|
||||
self, request: SDP_ServiceAttributeRequest
|
||||
) -> None:
|
||||
# Check if this is a continuation
|
||||
if len(request.continuation_state) > 1:
|
||||
if not self.current_response:
|
||||
if self.current_response is None:
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id=request.transaction_id,
|
||||
@@ -1069,22 +1130,24 @@ class Server:
|
||||
self.current_response = bytes(attribute_list)
|
||||
|
||||
# Respond, keeping any pending chunks for later
|
||||
attribute_list, continuation_state = self.get_next_response_payload(
|
||||
attribute_list_response, continuation_state = self.get_next_response_payload(
|
||||
request.maximum_attribute_byte_count
|
||||
)
|
||||
self.send_response(
|
||||
SDP_ServiceAttributeResponse(
|
||||
transaction_id=request.transaction_id,
|
||||
attribute_list_byte_count=len(attribute_list),
|
||||
attribute_list_byte_count=len(attribute_list_response),
|
||||
attribute_list=attribute_list,
|
||||
continuation_state=continuation_state,
|
||||
)
|
||||
)
|
||||
|
||||
def on_sdp_service_search_attribute_request(self, request):
|
||||
def on_sdp_service_search_attribute_request(
|
||||
self, request: SDP_ServiceSearchAttributeRequest
|
||||
) -> None:
|
||||
# Check if this is a continuation
|
||||
if len(request.continuation_state) > 1:
|
||||
if not self.current_response:
|
||||
if self.current_response is None:
|
||||
self.send_response(
|
||||
SDP_ErrorResponse(
|
||||
transaction_id=request.transaction_id,
|
||||
@@ -1114,13 +1177,13 @@ class Server:
|
||||
self.current_response = bytes(attribute_lists)
|
||||
|
||||
# Respond, keeping any pending chunks for later
|
||||
attribute_lists, continuation_state = self.get_next_response_payload(
|
||||
attribute_lists_response, continuation_state = self.get_next_response_payload(
|
||||
request.maximum_attribute_byte_count
|
||||
)
|
||||
self.send_response(
|
||||
SDP_ServiceSearchAttributeResponse(
|
||||
transaction_id=request.transaction_id,
|
||||
attribute_lists_byte_count=len(attribute_lists),
|
||||
attribute_lists_byte_count=len(attribute_lists_response),
|
||||
attribute_lists=attribute_lists,
|
||||
continuation_state=continuation_state,
|
||||
)
|
||||
|
||||
247
bumble/smp.py
247
bumble/smp.py
@@ -25,6 +25,7 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import asyncio
|
||||
import enum
|
||||
import secrets
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -36,6 +37,7 @@ from typing import (
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
cast,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
@@ -553,20 +555,16 @@ class AddressResolver:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Session:
|
||||
# Pairing methods
|
||||
class PairingMethod(enum.IntEnum):
|
||||
JUST_WORKS = 0
|
||||
NUMERIC_COMPARISON = 1
|
||||
PASSKEY = 2
|
||||
OOB = 3
|
||||
CTKD_OVER_CLASSIC = 4
|
||||
|
||||
PAIRING_METHOD_NAMES = {
|
||||
JUST_WORKS: 'JUST_WORKS',
|
||||
NUMERIC_COMPARISON: 'NUMERIC_COMPARISON',
|
||||
PASSKEY: 'PASSKEY',
|
||||
OOB: 'OOB',
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Session:
|
||||
# I/O Capability to pairing method decision matrix
|
||||
#
|
||||
# See Bluetooth spec @ Vol 3, part H - Table 2.8: Mapping of IO Capabilities to Key
|
||||
@@ -581,47 +579,50 @@ class Session:
|
||||
# (False).
|
||||
PAIRING_METHODS = {
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: {
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PASSKEY, True, False),
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: (PASSKEY, True, False),
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PairingMethod.PASSKEY, True, False),
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: (PairingMethod.PASSKEY, True, False),
|
||||
},
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: {
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: (JUST_WORKS, NUMERIC_COMPARISON),
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PASSKEY, True, False),
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: (
|
||||
PairingMethod.JUST_WORKS,
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
),
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PairingMethod.PASSKEY, True, False),
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: (
|
||||
(PASSKEY, True, False),
|
||||
NUMERIC_COMPARISON,
|
||||
(PairingMethod.PASSKEY, True, False),
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
),
|
||||
},
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: {
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: (PASSKEY, False, True),
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: (PASSKEY, False, True),
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PASSKEY, False, False),
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: (PASSKEY, False, True),
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: (PairingMethod.PASSKEY, False, True),
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: (PairingMethod.PASSKEY, False, True),
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PairingMethod.PASSKEY, False, False),
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: (PairingMethod.PASSKEY, False, True),
|
||||
},
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: {
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
},
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: {
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: (PASSKEY, False, True),
|
||||
SMP_DISPLAY_ONLY_IO_CAPABILITY: (PairingMethod.PASSKEY, False, True),
|
||||
SMP_DISPLAY_YES_NO_IO_CAPABILITY: (
|
||||
(PASSKEY, False, True),
|
||||
NUMERIC_COMPARISON,
|
||||
(PairingMethod.PASSKEY, False, True),
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
),
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PASSKEY, True, False),
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS,
|
||||
SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PairingMethod.PASSKEY, True, False),
|
||||
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: PairingMethod.JUST_WORKS,
|
||||
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: (
|
||||
(PASSKEY, True, False),
|
||||
NUMERIC_COMPARISON,
|
||||
(PairingMethod.PASSKEY, True, False),
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -664,7 +665,7 @@ class Session:
|
||||
self.passkey_ready = asyncio.Event()
|
||||
self.passkey_step = 0
|
||||
self.passkey_display = False
|
||||
self.pairing_method = 0
|
||||
self.pairing_method: PairingMethod = PairingMethod.JUST_WORKS
|
||||
self.pairing_config = pairing_config
|
||||
self.wait_before_continuing: Optional[asyncio.Future[None]] = None
|
||||
self.completed = False
|
||||
@@ -769,19 +770,23 @@ class Session:
|
||||
def decide_pairing_method(
|
||||
self, auth_req: int, initiator_io_capability: int, responder_io_capability: int
|
||||
) -> None:
|
||||
if self.connection.transport == BT_BR_EDR_TRANSPORT:
|
||||
self.pairing_method = PairingMethod.CTKD_OVER_CLASSIC
|
||||
return
|
||||
if (not self.mitm) and (auth_req & SMP_MITM_AUTHREQ == 0):
|
||||
self.pairing_method = self.JUST_WORKS
|
||||
self.pairing_method = PairingMethod.JUST_WORKS
|
||||
return
|
||||
|
||||
details = self.PAIRING_METHODS[initiator_io_capability][responder_io_capability] # type: ignore[index]
|
||||
if isinstance(details, tuple) and len(details) == 2:
|
||||
# One entry for legacy pairing and one for secure connections
|
||||
details = details[1 if self.sc else 0]
|
||||
if isinstance(details, int):
|
||||
if isinstance(details, PairingMethod):
|
||||
# Just a method ID
|
||||
self.pairing_method = details
|
||||
else:
|
||||
# PASSKEY method, with a method ID and display/input flags
|
||||
assert isinstance(details[0], PairingMethod)
|
||||
self.pairing_method = details[0]
|
||||
self.passkey_display = details[1 if self.is_initiator else 2]
|
||||
|
||||
@@ -858,10 +863,13 @@ class Session:
|
||||
self.tk = self.passkey.to_bytes(16, byteorder='little')
|
||||
logger.debug(f'TK from passkey = {self.tk.hex()}')
|
||||
|
||||
self.connection.abort_on(
|
||||
'disconnection',
|
||||
self.pairing_config.delegate.display_number(self.passkey, digits=6),
|
||||
)
|
||||
try:
|
||||
self.connection.abort_on(
|
||||
'disconnection',
|
||||
self.pairing_config.delegate.display_number(self.passkey, digits=6),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while displaying number: {error}')
|
||||
|
||||
def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
|
||||
# Prompt the user for the passkey displayed on the peer
|
||||
@@ -929,9 +937,12 @@ class Session:
|
||||
if self.sc:
|
||||
|
||||
async def next_steps() -> None:
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
if self.pairing_method in (
|
||||
PairingMethod.JUST_WORKS,
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
):
|
||||
z = 0
|
||||
elif self.pairing_method == self.PASSKEY:
|
||||
elif self.pairing_method == PairingMethod.PASSKEY:
|
||||
# We need a passkey
|
||||
await self.passkey_ready.wait()
|
||||
assert self.passkey
|
||||
@@ -983,6 +994,19 @@ class Session:
|
||||
)
|
||||
)
|
||||
|
||||
def send_identity_address_command(self) -> None:
|
||||
identity_address = {
|
||||
None: self.connection.self_address,
|
||||
Address.PUBLIC_DEVICE_ADDRESS: self.manager.device.public_address,
|
||||
Address.RANDOM_DEVICE_ADDRESS: self.manager.device.random_address,
|
||||
}[self.pairing_config.identity_address_type]
|
||||
self.send_command(
|
||||
SMP_Identity_Address_Information_Command(
|
||||
addr_type=identity_address.address_type,
|
||||
bd_addr=identity_address,
|
||||
)
|
||||
)
|
||||
|
||||
def start_encryption(self, key: bytes) -> None:
|
||||
# We can now encrypt the connection with the short term key, so that we can
|
||||
# distribute the long term and/or other keys over an encrypted connection
|
||||
@@ -1006,6 +1030,7 @@ class Session:
|
||||
self.ltk = crypto.h6(ilk, b'brle')
|
||||
|
||||
def distribute_keys(self) -> None:
|
||||
|
||||
# Distribute the keys as required
|
||||
if self.is_initiator:
|
||||
# CTKD: Derive LTK from LinkKey
|
||||
@@ -1035,12 +1060,7 @@ class Session:
|
||||
identity_resolving_key=self.manager.device.irk
|
||||
)
|
||||
)
|
||||
self.send_command(
|
||||
SMP_Identity_Address_Information_Command(
|
||||
addr_type=self.connection.self_address.address_type,
|
||||
bd_addr=self.connection.self_address,
|
||||
)
|
||||
)
|
||||
self.send_identity_address_command()
|
||||
|
||||
# Distribute CSRK
|
||||
csrk = bytes(16) # FIXME: testing
|
||||
@@ -1084,12 +1104,7 @@ class Session:
|
||||
identity_resolving_key=self.manager.device.irk
|
||||
)
|
||||
)
|
||||
self.send_command(
|
||||
SMP_Identity_Address_Information_Command(
|
||||
addr_type=self.connection.self_address.address_type,
|
||||
bd_addr=self.connection.self_address,
|
||||
)
|
||||
)
|
||||
self.send_identity_address_command()
|
||||
|
||||
# Distribute CSRK
|
||||
csrk = bytes(16) # FIXME: testing
|
||||
@@ -1224,7 +1239,7 @@ class Session:
|
||||
# Create an object to hold the keys
|
||||
keys = PairingKeys()
|
||||
keys.address_type = peer_address.address_type
|
||||
authenticated = self.pairing_method != self.JUST_WORKS
|
||||
authenticated = self.pairing_method != PairingMethod.JUST_WORKS
|
||||
if self.sc or self.connection.transport == BT_BR_EDR_TRANSPORT:
|
||||
keys.ltk = PairingKeys.Key(value=self.ltk, authenticated=authenticated)
|
||||
else:
|
||||
@@ -1258,7 +1273,7 @@ class Session:
|
||||
keys.link_key = PairingKeys.Key(
|
||||
value=self.link_key, authenticated=authenticated
|
||||
)
|
||||
self.manager.on_pairing(self, peer_address, keys)
|
||||
await self.manager.on_pairing(self, peer_address, keys)
|
||||
|
||||
def on_pairing_failure(self, reason: int) -> None:
|
||||
logger.warning(f'pairing failure ({error_name(reason)})')
|
||||
@@ -1300,7 +1315,11 @@ class Session:
|
||||
self, command: SMP_Pairing_Request_Command
|
||||
) -> None:
|
||||
# Check if the request should proceed
|
||||
accepted = await self.pairing_config.delegate.accept()
|
||||
try:
|
||||
accepted = await self.pairing_config.delegate.accept()
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while accepting: {error}')
|
||||
accepted = False
|
||||
if not accepted:
|
||||
logger.debug('pairing rejected by delegate')
|
||||
self.send_pairing_failed(SMP_PAIRING_NOT_SUPPORTED_ERROR)
|
||||
@@ -1323,9 +1342,7 @@ class Session:
|
||||
self.decide_pairing_method(
|
||||
command.auth_req, command.io_capability, self.io_capability
|
||||
)
|
||||
logger.debug(
|
||||
f'pairing method: {self.PAIRING_METHOD_NAMES[self.pairing_method]}'
|
||||
)
|
||||
logger.debug(f'pairing method: {self.pairing_method.name}')
|
||||
|
||||
# Key distribution
|
||||
(
|
||||
@@ -1341,7 +1358,7 @@ class Session:
|
||||
|
||||
# Display a passkey if we need to
|
||||
if not self.sc:
|
||||
if self.pairing_method == self.PASSKEY and self.passkey_display:
|
||||
if self.pairing_method == PairingMethod.PASSKEY and self.passkey_display:
|
||||
self.display_passkey()
|
||||
|
||||
# Respond
|
||||
@@ -1382,9 +1399,7 @@ class Session:
|
||||
self.decide_pairing_method(
|
||||
command.auth_req, self.io_capability, command.io_capability
|
||||
)
|
||||
logger.debug(
|
||||
f'pairing method: {self.PAIRING_METHOD_NAMES[self.pairing_method]}'
|
||||
)
|
||||
logger.debug(f'pairing method: {self.pairing_method.name}')
|
||||
|
||||
# Key distribution
|
||||
if (
|
||||
@@ -1400,13 +1415,16 @@ class Session:
|
||||
self.compute_peer_expected_distributions(self.responder_key_distribution)
|
||||
|
||||
# Start phase 2
|
||||
if self.sc:
|
||||
if self.pairing_method == self.PASSKEY:
|
||||
if self.pairing_method == PairingMethod.CTKD_OVER_CLASSIC:
|
||||
# Authentication is already done in SMP, so remote shall start keys distribution immediately
|
||||
return
|
||||
elif self.sc:
|
||||
if self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.display_or_input_passkey()
|
||||
|
||||
self.send_public_key_command()
|
||||
else:
|
||||
if self.pairing_method == self.PASSKEY:
|
||||
if self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.display_or_input_passkey(self.send_pairing_confirm_command)
|
||||
else:
|
||||
self.send_pairing_confirm_command()
|
||||
@@ -1418,7 +1436,10 @@ class Session:
|
||||
self.send_pairing_random_command()
|
||||
else:
|
||||
# If the method is PASSKEY, now is the time to input the code
|
||||
if self.pairing_method == self.PASSKEY and not self.passkey_display:
|
||||
if (
|
||||
self.pairing_method == PairingMethod.PASSKEY
|
||||
and not self.passkey_display
|
||||
):
|
||||
self.input_passkey(self.send_pairing_confirm_command)
|
||||
else:
|
||||
self.send_pairing_confirm_command()
|
||||
@@ -1426,11 +1447,14 @@ class Session:
|
||||
def on_smp_pairing_confirm_command_secure_connections(
|
||||
self, _: SMP_Pairing_Confirm_Command
|
||||
) -> None:
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
if self.pairing_method in (
|
||||
PairingMethod.JUST_WORKS,
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
):
|
||||
if self.is_initiator:
|
||||
self.r = crypto.r()
|
||||
self.send_pairing_random_command()
|
||||
elif self.pairing_method == self.PASSKEY:
|
||||
elif self.pairing_method == PairingMethod.PASSKEY:
|
||||
if self.is_initiator:
|
||||
self.send_pairing_random_command()
|
||||
else:
|
||||
@@ -1486,13 +1510,16 @@ class Session:
|
||||
def on_smp_pairing_random_command_secure_connections(
|
||||
self, command: SMP_Pairing_Random_Command
|
||||
) -> None:
|
||||
if self.pairing_method == self.PASSKEY and self.passkey is None:
|
||||
if self.pairing_method == PairingMethod.PASSKEY and self.passkey is None:
|
||||
logger.warning('no passkey entered, ignoring command')
|
||||
return
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
if self.is_initiator:
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
if self.pairing_method in (
|
||||
PairingMethod.JUST_WORKS,
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
):
|
||||
assert self.confirm_value
|
||||
# Check that the random value matches what was committed to earlier
|
||||
confirm_verifier = crypto.f4(
|
||||
@@ -1502,7 +1529,7 @@ class Session:
|
||||
self.confirm_value, confirm_verifier, SMP_CONFIRM_VALUE_FAILED_ERROR
|
||||
):
|
||||
return
|
||||
elif self.pairing_method == self.PASSKEY:
|
||||
elif self.pairing_method == PairingMethod.PASSKEY:
|
||||
assert self.passkey and self.confirm_value
|
||||
# Check that the random value matches what was committed to earlier
|
||||
confirm_verifier = crypto.f4(
|
||||
@@ -1525,9 +1552,12 @@ class Session:
|
||||
else:
|
||||
return
|
||||
else:
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
if self.pairing_method in (
|
||||
PairingMethod.JUST_WORKS,
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
):
|
||||
self.send_pairing_random_command()
|
||||
elif self.pairing_method == self.PASSKEY:
|
||||
elif self.pairing_method == PairingMethod.PASSKEY:
|
||||
assert self.passkey and self.confirm_value
|
||||
# Check that the random value matches what was committed to earlier
|
||||
confirm_verifier = crypto.f4(
|
||||
@@ -1558,10 +1588,13 @@ class Session:
|
||||
(mac_key, self.ltk) = crypto.f5(self.dh_key, self.na, self.nb, a, b)
|
||||
|
||||
# Compute the DH Key checks
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
if self.pairing_method in (
|
||||
PairingMethod.JUST_WORKS,
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
):
|
||||
ra = bytes(16)
|
||||
rb = ra
|
||||
elif self.pairing_method == self.PASSKEY:
|
||||
elif self.pairing_method == PairingMethod.PASSKEY:
|
||||
assert self.passkey
|
||||
ra = self.passkey.to_bytes(16, byteorder='little')
|
||||
rb = ra
|
||||
@@ -1585,13 +1618,16 @@ class Session:
|
||||
self.wait_before_continuing.set_result(None)
|
||||
|
||||
# Prompt the user for confirmation if needed
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
if self.pairing_method in (
|
||||
PairingMethod.JUST_WORKS,
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
):
|
||||
# Compute the 6-digit code
|
||||
code = crypto.g2(self.pka, self.pkb, self.na, self.nb) % 1000000
|
||||
|
||||
# Ask for user confirmation
|
||||
self.wait_before_continuing = asyncio.get_running_loop().create_future()
|
||||
if self.pairing_method == self.JUST_WORKS:
|
||||
if self.pairing_method == PairingMethod.JUST_WORKS:
|
||||
self.prompt_user_for_confirmation(next_steps)
|
||||
else:
|
||||
self.prompt_user_for_numeric_comparison(code, next_steps)
|
||||
@@ -1628,13 +1664,16 @@ class Session:
|
||||
if self.is_initiator:
|
||||
self.send_pairing_confirm_command()
|
||||
else:
|
||||
if self.pairing_method == self.PASSKEY:
|
||||
if self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.display_or_input_passkey()
|
||||
|
||||
# Send our public key back to the initiator
|
||||
self.send_public_key_command()
|
||||
|
||||
if self.pairing_method in (self.JUST_WORKS, self.NUMERIC_COMPARISON):
|
||||
if self.pairing_method in (
|
||||
PairingMethod.JUST_WORKS,
|
||||
PairingMethod.NUMERIC_COMPARISON,
|
||||
):
|
||||
# We can now send the confirmation value
|
||||
self.send_pairing_confirm_command()
|
||||
|
||||
@@ -1733,7 +1772,26 @@ class Manager(EventEmitter):
|
||||
cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID
|
||||
connection.send_l2cap_pdu(cid, command.to_bytes())
|
||||
|
||||
def on_smp_security_request_command(
|
||||
self, connection: Connection, request: SMP_Security_Request_Command
|
||||
) -> None:
|
||||
connection.emit('security_request', request.auth_req)
|
||||
|
||||
def on_smp_pdu(self, connection: Connection, pdu: bytes) -> None:
|
||||
# Parse the L2CAP payload into an SMP Command object
|
||||
command = SMP_Command.from_bytes(pdu)
|
||||
logger.debug(
|
||||
f'<<< Received SMP Command on connection [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address}: {command}'
|
||||
)
|
||||
|
||||
# Security request is more than just pairing, so let applications handle them
|
||||
if command.code == SMP_SECURITY_REQUEST_COMMAND:
|
||||
self.on_smp_security_request_command(
|
||||
connection, cast(SMP_Security_Request_Command, command)
|
||||
)
|
||||
return
|
||||
|
||||
# Look for a session with this connection, and create one if none exists
|
||||
if not (session := self.sessions.get(connection.handle)):
|
||||
if connection.role == BT_CENTRAL_ROLE:
|
||||
@@ -1744,13 +1802,6 @@ class Manager(EventEmitter):
|
||||
)
|
||||
self.sessions[connection.handle] = session
|
||||
|
||||
# Parse the L2CAP payload into an SMP Command object
|
||||
command = SMP_Command.from_bytes(pdu)
|
||||
logger.debug(
|
||||
f'<<< Received SMP Command on connection [0x{connection.handle:04X}] '
|
||||
f'{connection.peer_address}: {command}'
|
||||
)
|
||||
|
||||
# Delegate the handling of the command to the session
|
||||
session.on_smp_command(command)
|
||||
|
||||
@@ -1789,20 +1840,14 @@ class Manager(EventEmitter):
|
||||
def on_session_start(self, session: Session) -> None:
|
||||
self.device.on_pairing_start(session.connection)
|
||||
|
||||
def on_pairing(
|
||||
async def on_pairing(
|
||||
self, session: Session, identity_address: Optional[Address], keys: PairingKeys
|
||||
) -> None:
|
||||
# Store the keys in the key store
|
||||
if self.device.keystore and identity_address is not None:
|
||||
|
||||
async def store_keys():
|
||||
try:
|
||||
assert self.device.keystore
|
||||
await self.device.keystore.update(str(identity_address), keys)
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! error while storing keys: {error}')
|
||||
|
||||
self.device.abort_on('flush', store_keys())
|
||||
self.device.abort_on(
|
||||
'flush', self.device.update_keys(str(identity_address), keys)
|
||||
)
|
||||
|
||||
# Notify the device
|
||||
self.device.on_pairing(session.connection, identity_address, keys, session.sc)
|
||||
|
||||
@@ -20,7 +20,6 @@ import logging
|
||||
import os
|
||||
|
||||
from .common import Transport, AsyncPipeSink, SnoopingTransport
|
||||
from ..controller import Controller
|
||||
from ..snoop import create_snooper
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -69,6 +68,7 @@ async def open_transport(name: str) -> Transport:
|
||||
* usb
|
||||
* pyusb
|
||||
* android-emulator
|
||||
* android-netsim
|
||||
"""
|
||||
|
||||
return _wrap_transport(await _open_transport(name))
|
||||
@@ -118,7 +118,8 @@ async def _open_transport(name: str) -> Transport:
|
||||
if scheme == 'file':
|
||||
from .file import open_file_transport
|
||||
|
||||
return await open_file_transport(spec[0] if spec else None)
|
||||
assert spec is not None
|
||||
return await open_file_transport(spec[0])
|
||||
|
||||
if scheme == 'vhci':
|
||||
from .vhci import open_vhci_transport
|
||||
@@ -133,12 +134,14 @@ async def _open_transport(name: str) -> Transport:
|
||||
if scheme == 'usb':
|
||||
from .usb import open_usb_transport
|
||||
|
||||
return await open_usb_transport(spec[0] if spec else None)
|
||||
assert spec is not None
|
||||
return await open_usb_transport(spec[0])
|
||||
|
||||
if scheme == 'pyusb':
|
||||
from .pyusb import open_pyusb_transport
|
||||
|
||||
return await open_pyusb_transport(spec[0] if spec else None)
|
||||
assert spec is not None
|
||||
return await open_pyusb_transport(spec[0])
|
||||
|
||||
if scheme == 'android-emulator':
|
||||
from .android_emulator import open_android_emulator_transport
|
||||
@@ -167,6 +170,7 @@ async def open_transport_or_link(name: str) -> Transport:
|
||||
|
||||
"""
|
||||
if name.startswith('link-relay:'):
|
||||
from ..controller import Controller
|
||||
from ..link import RemoteLink # lazy import
|
||||
|
||||
link = RemoteLink(name[11:])
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
import logging
|
||||
import grpc.aio
|
||||
|
||||
from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink
|
||||
from typing import Optional, Union
|
||||
|
||||
from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink, Transport
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from .grpc_protobuf.emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
|
||||
@@ -33,7 +35,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_emulator_transport(spec):
|
||||
async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
|
||||
'''
|
||||
Open a transport connection to an Android emulator via its gRPC interface.
|
||||
The parameter string has this syntax:
|
||||
@@ -66,7 +68,7 @@ async def open_android_emulator_transport(spec):
|
||||
# Parse the parameters
|
||||
mode = 'host'
|
||||
server_host = 'localhost'
|
||||
server_port = 8554
|
||||
server_port = '8554'
|
||||
if spec is not None:
|
||||
params = spec.split(',')
|
||||
for param in params:
|
||||
@@ -82,6 +84,7 @@ async def open_android_emulator_transport(spec):
|
||||
logger.debug(f'connecting to gRPC server at {server_address}')
|
||||
channel = grpc.aio.insecure_channel(server_address)
|
||||
|
||||
service: Union[EmulatedBluetoothServiceStub, VhciForwardingServiceStub]
|
||||
if mode == 'host':
|
||||
# Connect as a host
|
||||
service = EmulatedBluetoothServiceStub(channel)
|
||||
@@ -94,10 +97,13 @@ async def open_android_emulator_transport(spec):
|
||||
raise ValueError('invalid mode')
|
||||
|
||||
# Create the transport object
|
||||
transport = PumpedTransport(
|
||||
PumpedPacketSource(hci_device.read),
|
||||
PumpedPacketSink(hci_device.write),
|
||||
channel.close,
|
||||
class EmulatorTransport(PumpedTransport):
|
||||
async def close(self):
|
||||
await super().close()
|
||||
await channel.close()
|
||||
|
||||
transport = EmulatorTransport(
|
||||
PumpedPacketSource(hci_device.read), PumpedPacketSink(hci_device.write)
|
||||
)
|
||||
transport.start()
|
||||
|
||||
|
||||
@@ -18,11 +18,12 @@
|
||||
import asyncio
|
||||
import atexit
|
||||
import logging
|
||||
import grpc.aio
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Optional
|
||||
from typing import Dict, Optional
|
||||
|
||||
import grpc.aio
|
||||
|
||||
from .common import (
|
||||
ParserSource,
|
||||
@@ -33,8 +34,8 @@ from .common import (
|
||||
)
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from .grpc_protobuf.packet_streamer_pb2_grpc import PacketStreamerStub
|
||||
from .grpc_protobuf.packet_streamer_pb2_grpc import (
|
||||
PacketStreamerStub,
|
||||
PacketStreamerServicer,
|
||||
add_PacketStreamerServicer_to_server,
|
||||
)
|
||||
@@ -43,6 +44,7 @@ from .grpc_protobuf.hci_packet_pb2 import HCIPacket
|
||||
from .grpc_protobuf.startup_pb2 import Chip, ChipInfo
|
||||
from .grpc_protobuf.common_pb2 import ChipKind
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -74,14 +76,20 @@ def get_ini_dir() -> Optional[pathlib.Path]:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def find_grpc_port() -> int:
|
||||
def ini_file_name(instance_number: int) -> str:
|
||||
suffix = f'_{instance_number}' if instance_number > 0 else ''
|
||||
return f'netsim{suffix}.ini'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def find_grpc_port(instance_number: int) -> int:
|
||||
if not (ini_dir := get_ini_dir()):
|
||||
logger.debug('no known directory for .ini file')
|
||||
return 0
|
||||
|
||||
ini_file = ini_dir / 'netsim.ini'
|
||||
ini_file = ini_dir / ini_file_name(instance_number)
|
||||
logger.debug(f'Looking for .ini file at {ini_file}')
|
||||
if ini_file.is_file():
|
||||
logger.debug(f'Found .ini file at {ini_file}')
|
||||
with open(ini_file, 'r') as ini_file_data:
|
||||
for line in ini_file_data.readlines():
|
||||
if '=' in line:
|
||||
@@ -90,12 +98,14 @@ def find_grpc_port() -> int:
|
||||
logger.debug(f'gRPC port = {value}')
|
||||
return int(value)
|
||||
|
||||
logger.debug('no grpc.port property found in .ini file')
|
||||
|
||||
# Not found
|
||||
return 0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def publish_grpc_port(grpc_port) -> bool:
|
||||
def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
|
||||
if not (ini_dir := get_ini_dir()):
|
||||
logger.debug('no known directory for .ini file')
|
||||
return False
|
||||
@@ -104,7 +114,7 @@ def publish_grpc_port(grpc_port) -> bool:
|
||||
logger.debug('ini directory does not exist')
|
||||
return False
|
||||
|
||||
ini_file = ini_dir / 'netsim.ini'
|
||||
ini_file = ini_dir / ini_file_name(instance_number)
|
||||
try:
|
||||
ini_file.write_text(f'grpc.port={grpc_port}\n')
|
||||
logger.debug(f"published gRPC port at {ini_file}")
|
||||
@@ -121,13 +131,16 @@ def publish_grpc_port(grpc_port) -> bool:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_netsim_controller_transport(server_host, server_port):
|
||||
async def open_android_netsim_controller_transport(
|
||||
server_host: Optional[str], server_port: int, options: Dict[str, str]
|
||||
) -> Transport:
|
||||
if not server_port:
|
||||
raise ValueError('invalid port')
|
||||
if server_host == '_' or not server_host:
|
||||
server_host = 'localhost'
|
||||
|
||||
if not publish_grpc_port(server_port):
|
||||
instance_number = int(options.get('instance', "0"))
|
||||
if not publish_grpc_port(server_port, instance_number):
|
||||
logger.warning("unable to publish gRPC port")
|
||||
|
||||
class HciDevice:
|
||||
@@ -184,15 +197,12 @@ async def open_android_netsim_controller_transport(server_host, server_port):
|
||||
logger.debug(f'<<< PACKET: {data.hex()}')
|
||||
self.on_data_received(data)
|
||||
|
||||
def send_packet(self, data):
|
||||
async def send():
|
||||
await self.context.write(
|
||||
PacketResponse(
|
||||
hci_packet=HCIPacket(packet_type=data[0], packet=data[1:])
|
||||
)
|
||||
async def send_packet(self, data):
|
||||
return await self.context.write(
|
||||
PacketResponse(
|
||||
hci_packet=HCIPacket(packet_type=data[0], packet=data[1:])
|
||||
)
|
||||
|
||||
self.loop.create_task(send())
|
||||
)
|
||||
|
||||
def terminate(self):
|
||||
self.task.cancel()
|
||||
@@ -226,17 +236,17 @@ async def open_android_netsim_controller_transport(server_host, server_port):
|
||||
logger.debug('gRPC server cancelled')
|
||||
await self.grpc_server.stop(None)
|
||||
|
||||
def on_packet(self, packet):
|
||||
async def send_packet(self, packet):
|
||||
if not self.device:
|
||||
logger.debug('no device, dropping packet')
|
||||
return
|
||||
|
||||
self.device.send_packet(packet)
|
||||
return await self.device.send_packet(packet)
|
||||
|
||||
async def StreamPackets(self, _request_iterator, context):
|
||||
logger.debug('StreamPackets request')
|
||||
|
||||
# Check that we won't already have a device
|
||||
# Check that we don't already have a device
|
||||
if self.device:
|
||||
logger.debug('busy, already serving a device')
|
||||
return PacketResponse(error='Busy')
|
||||
@@ -259,15 +269,42 @@ async def open_android_netsim_controller_transport(server_host, server_port):
|
||||
await server.start()
|
||||
asyncio.get_running_loop().create_task(server.serve())
|
||||
|
||||
class GrpcServerTransport(Transport):
|
||||
async def close(self):
|
||||
await super().close()
|
||||
|
||||
return GrpcServerTransport(server, server)
|
||||
sink = PumpedPacketSink(server.send_packet)
|
||||
sink.start()
|
||||
return Transport(server, sink)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_netsim_host_transport(server_host, server_port, options):
|
||||
async def open_android_netsim_host_transport_with_address(
|
||||
server_host: Optional[str],
|
||||
server_port: int,
|
||||
options: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
if server_host == '_' or not server_host:
|
||||
server_host = 'localhost'
|
||||
|
||||
if not server_port:
|
||||
# Look for the gRPC config in a .ini file
|
||||
instance_number = 0 if options is None else int(options.get('instance', '0'))
|
||||
server_port = find_grpc_port(instance_number)
|
||||
if not server_port:
|
||||
raise RuntimeError('gRPC server port not found')
|
||||
|
||||
# Connect to the gRPC server
|
||||
server_address = f'{server_host}:{server_port}'
|
||||
logger.debug(f'Connecting to gRPC server at {server_address}')
|
||||
channel = grpc.aio.insecure_channel(server_address)
|
||||
|
||||
return await open_android_netsim_host_transport_with_channel(
|
||||
channel,
|
||||
options,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_netsim_host_transport_with_channel(
|
||||
channel, options: Optional[Dict[str, str]] = None
|
||||
):
|
||||
# Wrapper for I/O operations
|
||||
class HciDevice:
|
||||
def __init__(self, name, manufacturer, hci_device):
|
||||
@@ -286,10 +323,12 @@ async def open_android_netsim_host_transport(server_host, server_port, options):
|
||||
async def read(self):
|
||||
response = await self.hci_device.read()
|
||||
response_type = response.WhichOneof('response_type')
|
||||
|
||||
if response_type == 'error':
|
||||
logger.warning(f'received error: {response.error}')
|
||||
raise RuntimeError(response.error)
|
||||
elif response_type == 'hci_packet':
|
||||
|
||||
if response_type == 'hci_packet':
|
||||
return (
|
||||
bytes([response.hci_packet.packet_type])
|
||||
+ response.hci_packet.packet
|
||||
@@ -304,24 +343,9 @@ async def open_android_netsim_host_transport(server_host, server_port, options):
|
||||
)
|
||||
)
|
||||
|
||||
name = options.get('name', DEFAULT_NAME)
|
||||
name = DEFAULT_NAME if options is None else options.get('name', DEFAULT_NAME)
|
||||
manufacturer = DEFAULT_MANUFACTURER
|
||||
|
||||
if server_host == '_' or not server_host:
|
||||
server_host = 'localhost'
|
||||
|
||||
if not server_port:
|
||||
# Look for the gRPC config in a .ini file
|
||||
server_host = 'localhost'
|
||||
server_port = find_grpc_port()
|
||||
if not server_port:
|
||||
raise RuntimeError('gRPC server port not found')
|
||||
|
||||
# Connect to the gRPC server
|
||||
server_address = f'{server_host}:{server_port}'
|
||||
logger.debug(f'Connecting to gRPC server at {server_address}')
|
||||
channel = grpc.aio.insecure_channel(server_address)
|
||||
|
||||
# Connect as a host
|
||||
service = PacketStreamerStub(channel)
|
||||
hci_device = HciDevice(
|
||||
@@ -332,10 +356,14 @@ async def open_android_netsim_host_transport(server_host, server_port, options):
|
||||
await hci_device.start()
|
||||
|
||||
# Create the transport object
|
||||
transport = PumpedTransport(
|
||||
class GrpcTransport(PumpedTransport):
|
||||
async def close(self):
|
||||
await super().close()
|
||||
await channel.close()
|
||||
|
||||
transport = GrpcTransport(
|
||||
PumpedPacketSource(hci_device.read),
|
||||
PumpedPacketSink(hci_device.write),
|
||||
channel.close,
|
||||
)
|
||||
transport.start()
|
||||
|
||||
@@ -343,7 +371,7 @@ async def open_android_netsim_host_transport(server_host, server_port, options):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_android_netsim_transport(spec):
|
||||
async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
|
||||
'''
|
||||
Open a transport connection as a client or server, implementing Android's `netsim`
|
||||
simulator protocol over gRPC.
|
||||
@@ -357,6 +385,11 @@ async def open_android_netsim_transport(spec):
|
||||
to connect *to* a netsim server (netsim is the controller), or accept
|
||||
connections *as* a netsim-compatible server.
|
||||
|
||||
instance=<n>
|
||||
Specifies an instance number, with <n> > 0. This is used to determine which
|
||||
.init file to use. In `host` mode, it is ignored when the <host>:<port>
|
||||
specifier is present, since in that case no .ini file is used.
|
||||
|
||||
In `host` mode:
|
||||
The <host>:<port> part is optional. When not specified, the transport
|
||||
looks for a netsim .ini file, from which it will read the `grpc.backend.port`
|
||||
@@ -385,14 +418,15 @@ async def open_android_netsim_transport(spec):
|
||||
params = spec.split(',') if spec else []
|
||||
if params and ':' in params[0]:
|
||||
# Explicit <host>:<port>
|
||||
host, port = params[0].split(':')
|
||||
host, port_str = params[0].split(':')
|
||||
port = int(port_str)
|
||||
params_offset = 1
|
||||
else:
|
||||
host = None
|
||||
port = 0
|
||||
params_offset = 0
|
||||
|
||||
options = {}
|
||||
options: Dict[str, str] = {}
|
||||
for param in params[params_offset:]:
|
||||
if '=' not in param:
|
||||
raise ValueError('invalid parameter, expected <name>=<value>')
|
||||
@@ -401,10 +435,12 @@ async def open_android_netsim_transport(spec):
|
||||
|
||||
mode = options.get('mode', 'host')
|
||||
if mode == 'host':
|
||||
return await open_android_netsim_host_transport(host, port, options)
|
||||
return await open_android_netsim_host_transport_with_address(
|
||||
host, port, options
|
||||
)
|
||||
if mode == 'controller':
|
||||
if host is None:
|
||||
raise ValueError('<host>:<port> missing')
|
||||
return await open_android_netsim_controller_transport(host, port)
|
||||
return await open_android_netsim_controller_transport(host, port, options)
|
||||
|
||||
raise ValueError('invalid mode option')
|
||||
|
||||
@@ -20,11 +20,12 @@ import contextlib
|
||||
import struct
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import ContextManager
|
||||
import io
|
||||
from typing import ContextManager, Tuple, Optional, Protocol, Dict
|
||||
|
||||
from .. import hci
|
||||
from ..colors import color
|
||||
from ..snoop import Snooper
|
||||
from bumble import hci
|
||||
from bumble.colors import color
|
||||
from bumble.snoop import Snooper
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -36,7 +37,7 @@ logger = logging.getLogger(__name__)
|
||||
# Information needed to parse HCI packets with a generic parser:
|
||||
# For each packet type, the info represents:
|
||||
# (length-size, length-offset, unpack-type)
|
||||
HCI_PACKET_INFO = {
|
||||
HCI_PACKET_INFO: Dict[int, Tuple[int, int, str]] = {
|
||||
hci.HCI_COMMAND_PACKET: (1, 2, 'B'),
|
||||
hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
|
||||
hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'),
|
||||
@@ -45,33 +46,54 @@ HCI_PACKET_INFO = {
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketPump:
|
||||
'''
|
||||
Pump HCI packets from a reader to a sink
|
||||
'''
|
||||
# Errors
|
||||
# -----------------------------------------------------------------------------
|
||||
class TransportLostError(Exception):
|
||||
"""
|
||||
The Transport has been lost/disconnected.
|
||||
"""
|
||||
|
||||
def __init__(self, reader, sink):
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Typing Protocols
|
||||
# -----------------------------------------------------------------------------
|
||||
class TransportSink(Protocol):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
...
|
||||
|
||||
|
||||
class TransportSource(Protocol):
|
||||
terminated: asyncio.Future[None]
|
||||
|
||||
def set_packet_sink(self, sink: TransportSink) -> None:
|
||||
...
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketPump:
|
||||
"""
|
||||
Pump HCI packets from a reader to a sink.
|
||||
"""
|
||||
|
||||
def __init__(self, reader: AsyncPacketReader, sink: TransportSink) -> None:
|
||||
self.reader = reader
|
||||
self.sink = sink
|
||||
|
||||
async def run(self):
|
||||
async def run(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
# Get a packet from the source
|
||||
packet = hci.HCI_Packet.from_bytes(await self.reader.next_packet())
|
||||
|
||||
# Deliver the packet to the sink
|
||||
self.sink.on_packet(packet)
|
||||
self.sink.on_packet(await self.reader.next_packet())
|
||||
except Exception as error:
|
||||
logger.warning(f'!!! {error}')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketParser:
|
||||
'''
|
||||
"""
|
||||
In-line parser that accepts data and emits 'on_packet' when a full packet has been
|
||||
parsed
|
||||
'''
|
||||
parsed.
|
||||
"""
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
|
||||
@@ -79,18 +101,22 @@ class PacketParser:
|
||||
NEED_LENGTH = 1
|
||||
NEED_BODY = 2
|
||||
|
||||
def __init__(self, sink=None):
|
||||
sink: Optional[TransportSink]
|
||||
extended_packet_info: Dict[int, Tuple[int, int, str]]
|
||||
packet_info: Optional[Tuple[int, int, str]] = None
|
||||
|
||||
def __init__(self, sink: Optional[TransportSink] = None) -> None:
|
||||
self.sink = sink
|
||||
self.extended_packet_info = {}
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> None:
|
||||
self.state = PacketParser.NEED_TYPE
|
||||
self.bytes_needed = 1
|
||||
self.packet = bytearray()
|
||||
self.packet_info = None
|
||||
|
||||
def feed_data(self, data):
|
||||
def feed_data(self, data: bytes) -> None:
|
||||
data_offset = 0
|
||||
data_left = len(data)
|
||||
while data_left and self.bytes_needed:
|
||||
@@ -111,6 +137,7 @@ class PacketParser:
|
||||
self.state = PacketParser.NEED_LENGTH
|
||||
self.bytes_needed = self.packet_info[0] + self.packet_info[1]
|
||||
elif self.state == PacketParser.NEED_LENGTH:
|
||||
assert self.packet_info is not None
|
||||
body_length = struct.unpack_from(
|
||||
self.packet_info[2], self.packet, 1 + self.packet_info[1]
|
||||
)[0]
|
||||
@@ -128,20 +155,20 @@ class PacketParser:
|
||||
)
|
||||
self.reset()
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
def set_packet_sink(self, sink: TransportSink) -> None:
|
||||
self.sink = sink
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PacketReader:
|
||||
'''
|
||||
Reader that reads HCI packets from a sync source
|
||||
'''
|
||||
"""
|
||||
Reader that reads HCI packets from a sync source.
|
||||
"""
|
||||
|
||||
def __init__(self, source):
|
||||
def __init__(self, source: io.BufferedReader) -> None:
|
||||
self.source = source
|
||||
|
||||
def next_packet(self):
|
||||
def next_packet(self) -> Optional[bytes]:
|
||||
# Get the packet type
|
||||
packet_type = self.source.read(1)
|
||||
if len(packet_type) != 1:
|
||||
@@ -150,7 +177,7 @@ class PacketReader:
|
||||
# Get the packet info based on its type
|
||||
packet_info = HCI_PACKET_INFO.get(packet_type[0])
|
||||
if packet_info is None:
|
||||
raise ValueError(f'invalid packet type {packet_type} found')
|
||||
raise ValueError(f'invalid packet type {packet_type[0]} found')
|
||||
|
||||
# Read the header (that includes the length)
|
||||
header_size = packet_info[0] + packet_info[1]
|
||||
@@ -169,21 +196,21 @@ class PacketReader:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AsyncPacketReader:
|
||||
'''
|
||||
Reader that reads HCI packets from an async source
|
||||
'''
|
||||
"""
|
||||
Reader that reads HCI packets from an async source.
|
||||
"""
|
||||
|
||||
def __init__(self, source):
|
||||
def __init__(self, source: asyncio.StreamReader) -> None:
|
||||
self.source = source
|
||||
|
||||
async def next_packet(self):
|
||||
async def next_packet(self) -> bytes:
|
||||
# Get the packet type
|
||||
packet_type = await self.source.readexactly(1)
|
||||
|
||||
# Get the packet info based on its type
|
||||
packet_info = HCI_PACKET_INFO.get(packet_type[0])
|
||||
if packet_info is None:
|
||||
raise ValueError(f'invalid packet type {packet_type} found')
|
||||
raise ValueError(f'invalid packet type {packet_type[0]} found')
|
||||
|
||||
# Read the header (that includes the length)
|
||||
header_size = packet_info[0] + packet_info[1]
|
||||
@@ -198,15 +225,15 @@ class AsyncPacketReader:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AsyncPipeSink:
|
||||
'''
|
||||
Sink that forwards packets asynchronously to another sink
|
||||
'''
|
||||
"""
|
||||
Sink that forwards packets asynchronously to another sink.
|
||||
"""
|
||||
|
||||
def __init__(self, sink):
|
||||
def __init__(self, sink: TransportSink) -> None:
|
||||
self.sink = sink
|
||||
self.loop = asyncio.get_running_loop()
|
||||
|
||||
def on_packet(self, packet):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
self.loop.call_soon(self.sink.on_packet, packet)
|
||||
|
||||
|
||||
@@ -216,35 +243,48 @@ class ParserSource:
|
||||
Base class designed to be subclassed by transport-specific source classes
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
terminated: asyncio.Future[None]
|
||||
parser: PacketParser
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.parser = PacketParser()
|
||||
self.terminated = asyncio.get_running_loop().create_future()
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
def set_packet_sink(self, sink: TransportSink) -> None:
|
||||
self.parser.set_packet_sink(sink)
|
||||
|
||||
async def wait_for_termination(self):
|
||||
def on_transport_lost(self) -> None:
|
||||
self.terminated.set_result(None)
|
||||
if self.parser.sink:
|
||||
if hasattr(self.parser.sink, 'on_transport_lost'):
|
||||
self.parser.sink.on_transport_lost()
|
||||
|
||||
async def wait_for_termination(self) -> None:
|
||||
"""
|
||||
Convenience method for backward compatibility. Prefer using the `terminated`
|
||||
attribute instead.
|
||||
"""
|
||||
return await self.terminated
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class StreamPacketSource(asyncio.Protocol, ParserSource):
|
||||
def data_received(self, data):
|
||||
def data_received(self, data: bytes) -> None:
|
||||
self.parser.feed_data(data)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class StreamPacketSink:
|
||||
def __init__(self, transport):
|
||||
def __init__(self, transport: asyncio.WriteTransport) -> None:
|
||||
self.transport = transport
|
||||
|
||||
def on_packet(self, packet):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
self.transport.write(packet)
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
self.transport.close()
|
||||
|
||||
|
||||
@@ -264,7 +304,7 @@ class Transport:
|
||||
...
|
||||
"""
|
||||
|
||||
def __init__(self, source, sink):
|
||||
def __init__(self, source: TransportSource, sink: TransportSink) -> None:
|
||||
self.source = source
|
||||
self.sink = sink
|
||||
|
||||
@@ -278,34 +318,39 @@ class Transport:
|
||||
return iter((self.source, self.sink))
|
||||
|
||||
async def close(self) -> None:
|
||||
self.source.close()
|
||||
self.sink.close()
|
||||
if hasattr(self.source, 'close'):
|
||||
self.source.close()
|
||||
if hasattr(self.sink, 'close'):
|
||||
self.sink.close()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PumpedPacketSource(ParserSource):
|
||||
def __init__(self, receive):
|
||||
pump_task: Optional[asyncio.Task[None]]
|
||||
|
||||
def __init__(self, receive) -> None:
|
||||
super().__init__()
|
||||
self.receive_function = receive
|
||||
self.pump_task = None
|
||||
|
||||
def start(self):
|
||||
async def pump_packets():
|
||||
def start(self) -> None:
|
||||
async def pump_packets() -> None:
|
||||
while True:
|
||||
try:
|
||||
packet = await self.receive_function()
|
||||
self.parser.feed_data(packet)
|
||||
except asyncio.exceptions.CancelledError:
|
||||
except asyncio.CancelledError:
|
||||
logger.debug('source pump task done')
|
||||
self.terminated.set_result(None)
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while waiting for packet: {error}')
|
||||
self.terminated.set_result(error)
|
||||
self.terminated.set_exception(error)
|
||||
break
|
||||
|
||||
self.pump_task = asyncio.create_task(pump_packets())
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
if self.pump_task:
|
||||
self.pump_task.cancel()
|
||||
|
||||
@@ -317,7 +362,7 @@ class PumpedPacketSink:
|
||||
self.packet_queue = asyncio.Queue()
|
||||
self.pump_task = None
|
||||
|
||||
def on_packet(self, packet):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
self.packet_queue.put_nowait(packet)
|
||||
|
||||
def start(self):
|
||||
@@ -326,7 +371,7 @@ class PumpedPacketSink:
|
||||
try:
|
||||
packet = await self.packet_queue.get()
|
||||
await self.send_function(packet)
|
||||
except asyncio.exceptions.CancelledError:
|
||||
except asyncio.CancelledError:
|
||||
logger.debug('sink pump task done')
|
||||
break
|
||||
except Exception as error:
|
||||
@@ -342,18 +387,20 @@ class PumpedPacketSink:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class PumpedTransport(Transport):
|
||||
def __init__(self, source, sink, close_function):
|
||||
super().__init__(source, sink)
|
||||
self.close_function = close_function
|
||||
source: PumpedPacketSource
|
||||
sink: PumpedPacketSink
|
||||
|
||||
def start(self):
|
||||
def __init__(
|
||||
self,
|
||||
source: PumpedPacketSource,
|
||||
sink: PumpedPacketSink,
|
||||
) -> None:
|
||||
super().__init__(source, sink)
|
||||
|
||||
def start(self) -> None:
|
||||
self.source.start()
|
||||
self.sink.start()
|
||||
|
||||
async def close(self):
|
||||
await super().close()
|
||||
await self.close_function()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SnoopingTransport(Transport):
|
||||
@@ -375,31 +422,38 @@ class SnoopingTransport(Transport):
|
||||
raise RuntimeError('unexpected code path') # Satisfy the type checker
|
||||
|
||||
class Source:
|
||||
def __init__(self, source, snooper):
|
||||
sink: TransportSink
|
||||
|
||||
def __init__(self, source: TransportSource, snooper: Snooper):
|
||||
self.source = source
|
||||
self.snooper = snooper
|
||||
self.sink = None
|
||||
self.terminated = source.terminated
|
||||
|
||||
def set_packet_sink(self, sink):
|
||||
def set_packet_sink(self, sink: TransportSink) -> None:
|
||||
self.sink = sink
|
||||
self.source.set_packet_sink(self)
|
||||
|
||||
def on_packet(self, packet):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
self.snooper.snoop(packet, Snooper.Direction.CONTROLLER_TO_HOST)
|
||||
if self.sink:
|
||||
self.sink.on_packet(packet)
|
||||
|
||||
class Sink:
|
||||
def __init__(self, sink, snooper):
|
||||
def __init__(self, sink: TransportSink, snooper: Snooper) -> None:
|
||||
self.sink = sink
|
||||
self.snooper = snooper
|
||||
|
||||
def on_packet(self, packet):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
self.snooper.snoop(packet, Snooper.Direction.HOST_TO_CONTROLLER)
|
||||
if self.sink:
|
||||
self.sink.on_packet(packet)
|
||||
|
||||
def __init__(self, transport, snooper, close_snooper=None):
|
||||
def __init__(
|
||||
self,
|
||||
transport: Transport,
|
||||
snooper: Snooper,
|
||||
close_snooper=None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
self.Source(transport.source, snooper), self.Sink(transport.sink, snooper)
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_file_transport(spec):
|
||||
async def open_file_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a File transport (typically not for a real file, but for a PTY or other unix
|
||||
virtual files).
|
||||
|
||||
@@ -23,6 +23,8 @@ import socket
|
||||
import ctypes
|
||||
import collections
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport, ParserSource
|
||||
|
||||
|
||||
@@ -33,7 +35,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_hci_socket_transport(spec):
|
||||
async def open_hci_socket_transport(spec: Optional[str]) -> Transport:
|
||||
'''
|
||||
Open an HCI Socket (only available on some platforms).
|
||||
The parameter string is either empty (to use the first/default Bluetooth adapter)
|
||||
@@ -45,9 +47,9 @@ async def open_hci_socket_transport(spec):
|
||||
# Create a raw HCI socket
|
||||
try:
|
||||
hci_socket = socket.socket(
|
||||
socket.AF_BLUETOOTH,
|
||||
socket.SOCK_RAW | socket.SOCK_NONBLOCK,
|
||||
socket.BTPROTO_HCI,
|
||||
socket.AF_BLUETOOTH, # type: ignore[attr-defined]
|
||||
socket.SOCK_RAW | socket.SOCK_NONBLOCK, # type: ignore[attr-defined]
|
||||
socket.BTPROTO_HCI, # type: ignore[attr-defined]
|
||||
)
|
||||
except AttributeError as error:
|
||||
# Not supported on this platform
|
||||
@@ -78,7 +80,7 @@ async def open_hci_socket_transport(spec):
|
||||
bind_address = struct.pack(
|
||||
# pylint: disable=no-member
|
||||
'<HHH',
|
||||
socket.AF_BLUETOOTH,
|
||||
socket.AF_BLUETOOTH, # type: ignore[attr-defined]
|
||||
adapter_index,
|
||||
HCI_CHANNEL_USER,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,8 @@ import atexit
|
||||
import os
|
||||
import logging
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport, StreamPacketSource, StreamPacketSink
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -32,7 +34,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_pty_transport(spec):
|
||||
async def open_pty_transport(spec: Optional[str]) -> Transport:
|
||||
'''
|
||||
Open a PTY transport.
|
||||
The parameter string may be empty, or a path name where a symbolic link
|
||||
|
||||
@@ -35,7 +35,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_pyusb_transport(spec):
|
||||
async def open_pyusb_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a USB transport. [Implementation based on PyUSB]
|
||||
The parameter string has this syntax:
|
||||
|
||||
@@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_serial_transport(spec):
|
||||
async def open_serial_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a serial port transport.
|
||||
The parameter string has this syntax:
|
||||
|
||||
@@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_tcp_client_transport(spec):
|
||||
async def open_tcp_client_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a TCP client transport.
|
||||
The parameter string has this syntax:
|
||||
@@ -39,7 +39,7 @@ async def open_tcp_client_transport(spec):
|
||||
class TcpPacketSource(StreamPacketSource):
|
||||
def connection_lost(self, exc):
|
||||
logger.debug(f'connection lost: {exc}')
|
||||
self.terminated.set_result(exc)
|
||||
self.on_transport_lost()
|
||||
|
||||
remote_host, remote_port = spec.split(':')
|
||||
tcp_transport, packet_source = await asyncio.get_running_loop().create_connection(
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
@@ -27,7 +28,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_tcp_server_transport(spec):
|
||||
async def open_tcp_server_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a TCP server transport.
|
||||
The parameter string has this syntax:
|
||||
@@ -42,7 +43,7 @@ async def open_tcp_server_transport(spec):
|
||||
async def close(self):
|
||||
await super().close()
|
||||
|
||||
class TcpServerProtocol:
|
||||
class TcpServerProtocol(asyncio.BaseProtocol):
|
||||
def __init__(self, packet_source, packet_sink):
|
||||
self.packet_source = packet_source
|
||||
self.packet_sink = packet_sink
|
||||
|
||||
@@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_udp_transport(spec):
|
||||
async def open_udp_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a UDP transport.
|
||||
The parameter string has this syntax:
|
||||
|
||||
@@ -60,7 +60,7 @@ def load_libusb():
|
||||
usb1.loadLibrary(libusb_dll)
|
||||
|
||||
|
||||
async def open_usb_transport(spec):
|
||||
async def open_usb_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a USB transport.
|
||||
The moniker string has this syntax:
|
||||
@@ -206,10 +206,11 @@ async def open_usb_transport(spec):
|
||||
logger.debug('OUT transfer likely already completed')
|
||||
|
||||
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||
def __init__(self, context, device, acl_in, events_in):
|
||||
def __init__(self, context, device, metadata, acl_in, events_in):
|
||||
super().__init__()
|
||||
self.context = context
|
||||
self.device = device
|
||||
self.metadata = metadata
|
||||
self.acl_in = acl_in
|
||||
self.events_in = events_in
|
||||
self.loop = asyncio.get_running_loop()
|
||||
@@ -510,6 +511,10 @@ async def open_usb_transport(spec):
|
||||
f'events_in=0x{events_in:02X}, '
|
||||
)
|
||||
|
||||
device_metadata = {
|
||||
'vendor_id': found.getVendorID(),
|
||||
'product_id': found.getProductID(),
|
||||
}
|
||||
device = found.open()
|
||||
|
||||
# Auto-detach the kernel driver if supported
|
||||
@@ -535,7 +540,7 @@ async def open_usb_transport(spec):
|
||||
except usb1.USBError:
|
||||
logger.warning('failed to set configuration')
|
||||
|
||||
source = UsbPacketSource(context, device, acl_in, events_in)
|
||||
source = UsbPacketSource(context, device, device_metadata, acl_in, events_in)
|
||||
sink = UsbPacketSink(device, acl_out)
|
||||
return UsbTransport(context, device, interface, setting, source, sink)
|
||||
except usb1.USBError as error:
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .common import Transport
|
||||
from .file import open_file_transport
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -26,7 +29,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_vhci_transport(spec):
|
||||
async def open_vhci_transport(spec: Optional[str]) -> Transport:
|
||||
'''
|
||||
Open a VHCI transport (only available on some platforms).
|
||||
The parameter string is either empty (to use the default VHCI device
|
||||
@@ -42,15 +45,15 @@ async def open_vhci_transport(spec):
|
||||
# Override the source's `data_received` method so that we can
|
||||
# filter out the vendor packet that is received just after the
|
||||
# initial open
|
||||
def vhci_data_received(data):
|
||||
def vhci_data_received(data: bytes) -> None:
|
||||
if len(data) > 0 and data[0] == HCI_VENDOR_PKT:
|
||||
if len(data) == 4:
|
||||
hci_index = data[2] << 8 | data[3]
|
||||
logger.info(f'HCI index {hci_index}')
|
||||
else:
|
||||
transport.source.parser.feed_data(data)
|
||||
transport.source.parser.feed_data(data) # type: ignore
|
||||
|
||||
transport.source.data_received = vhci_data_received
|
||||
transport.source.data_received = vhci_data_received # type: ignore
|
||||
|
||||
# Write the initial config
|
||||
transport.sink.on_packet(bytes([HCI_VENDOR_PKT, HCI_BREDR]))
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import websockets
|
||||
import websockets.client
|
||||
|
||||
from .common import PumpedPacketSource, PumpedPacketSink, PumpedTransport
|
||||
from .common import PumpedPacketSource, PumpedPacketSink, PumpedTransport, Transport
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -27,23 +27,25 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_ws_client_transport(spec):
|
||||
async def open_ws_client_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a WebSocket client transport.
|
||||
The parameter string has this syntax:
|
||||
<remote-host>:<remote-port>
|
||||
<websocket-url>
|
||||
|
||||
Example: 127.0.0.1:9001
|
||||
Example: ws://localhost:7681/v1/websocket/bt
|
||||
'''
|
||||
|
||||
remote_host, remote_port = spec.split(':')
|
||||
uri = f'ws://{remote_host}:{remote_port}'
|
||||
websocket = await websockets.connect(uri)
|
||||
websocket = await websockets.client.connect(spec)
|
||||
|
||||
transport = PumpedTransport(
|
||||
class WsTransport(PumpedTransport):
|
||||
async def close(self):
|
||||
await super().close()
|
||||
await websocket.close()
|
||||
|
||||
transport = WsTransport(
|
||||
PumpedPacketSource(websocket.recv),
|
||||
PumpedPacketSink(websocket.send),
|
||||
websocket.close,
|
||||
)
|
||||
transport.start()
|
||||
return transport
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
import websockets
|
||||
|
||||
@@ -28,7 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_ws_server_transport(spec):
|
||||
async def open_ws_server_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a WebSocket server transport.
|
||||
The parameter string has this syntax:
|
||||
@@ -43,7 +42,7 @@ async def open_ws_server_transport(spec):
|
||||
def __init__(self):
|
||||
source = ParserSource()
|
||||
sink = PumpedPacketSink(self.send_packet)
|
||||
self.connection = asyncio.get_running_loop().create_future()
|
||||
self.connection = None
|
||||
self.server = None
|
||||
|
||||
super().__init__(source, sink)
|
||||
@@ -63,7 +62,7 @@ async def open_ws_server_transport(spec):
|
||||
f'new connection on {connection.local_address} '
|
||||
f'from {connection.remote_address}'
|
||||
)
|
||||
self.connection.set_result(connection)
|
||||
self.connection = connection
|
||||
# pylint: disable=no-member
|
||||
try:
|
||||
async for packet in connection:
|
||||
@@ -74,12 +73,14 @@ async def open_ws_server_transport(spec):
|
||||
except websockets.WebSocketException as error:
|
||||
logger.debug(f'exception while receiving packet: {error}')
|
||||
|
||||
# Wait for a new connection
|
||||
self.connection = asyncio.get_running_loop().create_future()
|
||||
# We're now disconnected
|
||||
self.connection = None
|
||||
|
||||
async def send_packet(self, packet):
|
||||
connection = await self.connection
|
||||
return await connection.send(packet)
|
||||
if self.connection is None:
|
||||
logger.debug('no connection, dropping packet')
|
||||
return
|
||||
return await self.connection.send(packet)
|
||||
|
||||
local_host, local_port = spec.split(':')
|
||||
transport = WsServerTransport()
|
||||
|
||||
129
bumble/utils.py
129
bumble/utils.py
@@ -15,13 +15,25 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
import collections
|
||||
import sys
|
||||
from typing import Awaitable, Set, TypeVar
|
||||
from functools import wraps
|
||||
from typing import (
|
||||
Awaitable,
|
||||
Set,
|
||||
TypeVar,
|
||||
List,
|
||||
Tuple,
|
||||
Callable,
|
||||
Any,
|
||||
Optional,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
from functools import wraps, partial
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .colors import color
|
||||
@@ -64,6 +76,102 @@ def composite_listener(cls):
|
||||
return cls
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
_Handler = TypeVar('_Handler', bound=Callable)
|
||||
|
||||
|
||||
class EventWatcher:
|
||||
'''A wrapper class to control the lifecycle of event handlers better.
|
||||
|
||||
Usage:
|
||||
```
|
||||
watcher = EventWatcher()
|
||||
|
||||
def on_foo():
|
||||
...
|
||||
watcher.on(emitter, 'foo', on_foo)
|
||||
|
||||
@watcher.on(emitter, 'bar')
|
||||
def on_bar():
|
||||
...
|
||||
|
||||
# Close all event handlers watching through this watcher
|
||||
watcher.close()
|
||||
```
|
||||
|
||||
As context:
|
||||
```
|
||||
with contextlib.closing(EventWatcher()) as context:
|
||||
@context.on(emitter, 'foo')
|
||||
def on_foo():
|
||||
...
|
||||
# on_foo() has been removed here!
|
||||
```
|
||||
'''
|
||||
|
||||
handlers: List[Tuple[EventEmitter, str, Callable[..., Any]]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.handlers = []
|
||||
|
||||
@overload
|
||||
def on(self, emitter: EventEmitter, event: str) -> Callable[[_Handler], _Handler]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def on(self, emitter: EventEmitter, event: str, handler: _Handler) -> _Handler:
|
||||
...
|
||||
|
||||
def on(
|
||||
self, emitter: EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
|
||||
'''Watch an event until the context is closed.
|
||||
|
||||
Args:
|
||||
emitter: EventEmitter to watch
|
||||
event: Event name
|
||||
handler: (Optional) Event handler. When nothing is passed, this method works as a decorator.
|
||||
'''
|
||||
|
||||
def wrapper(f: _Handler) -> _Handler:
|
||||
self.handlers.append((emitter, event, f))
|
||||
emitter.on(event, f)
|
||||
return f
|
||||
|
||||
return wrapper if handler is None else wrapper(handler)
|
||||
|
||||
@overload
|
||||
def once(self, emitter: EventEmitter, event: str) -> Callable[[_Handler], _Handler]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def once(self, emitter: EventEmitter, event: str, handler: _Handler) -> _Handler:
|
||||
...
|
||||
|
||||
def once(
|
||||
self, emitter: EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
|
||||
'''Watch an event for once.
|
||||
|
||||
Args:
|
||||
emitter: EventEmitter to watch
|
||||
event: Event name
|
||||
handler: (Optional) Event handler. When nothing passed, this method works as a decorator.
|
||||
'''
|
||||
|
||||
def wrapper(f: _Handler) -> _Handler:
|
||||
self.handlers.append((emitter, event, f))
|
||||
emitter.once(event, f)
|
||||
return f
|
||||
|
||||
return wrapper if handler is None else wrapper(handler)
|
||||
|
||||
def close(self) -> None:
|
||||
for emitter, event, handler in self.handlers:
|
||||
if handler in emitter.listeners(event):
|
||||
emitter.remove_listener(event, handler)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
_T = TypeVar('_T')
|
||||
|
||||
@@ -302,3 +410,20 @@ class FlowControlAsyncPipe:
|
||||
self.resume_source()
|
||||
|
||||
self.check_pump()
|
||||
|
||||
|
||||
async def async_call(function, *args, **kwargs):
|
||||
"""
|
||||
Immediately calls the function with provided args and kwargs, wrapping it in an async function.
|
||||
Rust's `pyo3_asyncio` library needs functions to be marked async to properly inject a running loop.
|
||||
|
||||
result = await async_call(some_function, ...)
|
||||
"""
|
||||
return function(*args, **kwargs)
|
||||
|
||||
|
||||
def wrap_async(function):
|
||||
"""
|
||||
Wraps the provided function in an async function.
|
||||
"""
|
||||
return partial(async_call, function)
|
||||
|
||||
0
bumble/vendor/__init__.py
vendored
Normal file
0
bumble/vendor/__init__.py
vendored
Normal file
0
bumble/vendor/android/__init__.py
vendored
Normal file
0
bumble/vendor/android/__init__.py
vendored
Normal file
318
bumble/vendor/android/hci.py
vendored
Normal file
318
bumble/vendor/android/hci.py
vendored
Normal file
@@ -0,0 +1,318 @@
|
||||
# Copyright 2021-2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
|
||||
from bumble.hci import (
|
||||
name_or_number,
|
||||
hci_vendor_command_op_code,
|
||||
Address,
|
||||
HCI_Constant,
|
||||
HCI_Object,
|
||||
HCI_Command,
|
||||
HCI_Vendor_Event,
|
||||
STATUS_SPEC,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Android Vendor Specific Commands and Events.
|
||||
# Only a subset of the commands are implemented here currently.
|
||||
#
|
||||
# pylint: disable-next=line-too-long
|
||||
# See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#chip-capabilities-and-configuration
|
||||
HCI_LE_GET_VENDOR_CAPABILITIES_COMMAND = hci_vendor_command_op_code(0x153)
|
||||
HCI_LE_APCF_COMMAND = hci_vendor_command_op_code(0x157)
|
||||
HCI_GET_CONTROLLER_ACTIVITY_ENERGY_INFO_COMMAND = hci_vendor_command_op_code(0x159)
|
||||
HCI_A2DP_HARDWARE_OFFLOAD_COMMAND = hci_vendor_command_op_code(0x15D)
|
||||
HCI_BLUETOOTH_QUALITY_REPORT_COMMAND = hci_vendor_command_op_code(0x15E)
|
||||
HCI_DYNAMIC_AUDIO_BUFFER_COMMAND = hci_vendor_command_op_code(0x15F)
|
||||
|
||||
HCI_BLUETOOTH_QUALITY_REPORT_EVENT = 0x58
|
||||
|
||||
HCI_Command.register_commands(globals())
|
||||
HCI_Vendor_Event.register_subevents(globals())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('max_advt_instances', 1),
|
||||
('offloaded_resolution_of_private_address', 1),
|
||||
('total_scan_results_storage', 2),
|
||||
('max_irk_list_sz', 1),
|
||||
('filtering_support', 1),
|
||||
('max_filter', 1),
|
||||
('activity_energy_info_support', 1),
|
||||
('version_supported', 2),
|
||||
('total_num_of_advt_tracked', 2),
|
||||
('extended_scan_support', 1),
|
||||
('debug_logging_supported', 1),
|
||||
('le_address_generation_offloading_support', 1),
|
||||
('a2dp_source_offload_capability_mask', 4),
|
||||
('bluetooth_quality_report_support', 1),
|
||||
('dynamic_audio_buffer_support', 4),
|
||||
]
|
||||
)
|
||||
class HCI_LE_Get_Vendor_Capabilities_Command(HCI_Command):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#vendor-specific-capabilities
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def parse_return_parameters(cls, parameters):
|
||||
# There are many versions of this data structure, so we need to parse until
|
||||
# there are no more bytes to parse, and leave un-signal parameters set to
|
||||
# None (older versions)
|
||||
nones = {field: None for field, _ in cls.return_parameters_fields}
|
||||
return_parameters = HCI_Object(cls.return_parameters_fields, **nones)
|
||||
|
||||
try:
|
||||
offset = 0
|
||||
for field in cls.return_parameters_fields:
|
||||
field_name, field_type = field
|
||||
field_value, field_size = HCI_Object.parse_field(
|
||||
parameters, offset, field_type
|
||||
)
|
||||
setattr(return_parameters, field_name, field_value)
|
||||
offset += field_size
|
||||
except struct.error:
|
||||
pass
|
||||
|
||||
return return_parameters
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
(
|
||||
'opcode',
|
||||
{
|
||||
'size': 1,
|
||||
'mapper': lambda x: HCI_LE_APCF_Command.opcode_name(x),
|
||||
},
|
||||
),
|
||||
('payload', '*'),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
(
|
||||
'opcode',
|
||||
{
|
||||
'size': 1,
|
||||
'mapper': lambda x: HCI_LE_APCF_Command.opcode_name(x),
|
||||
},
|
||||
),
|
||||
('payload', '*'),
|
||||
],
|
||||
)
|
||||
class HCI_LE_APCF_Command(HCI_Command):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_apcf_command
|
||||
|
||||
NOTE: the subcommand-specific payloads are left as opaque byte arrays in this
|
||||
implementation. A future enhancement may define subcommand-specific data structures.
|
||||
'''
|
||||
|
||||
# APCF Subcommands
|
||||
# TODO: use the OpenIntEnum class (when upcoming PR is merged)
|
||||
APCF_ENABLE = 0x00
|
||||
APCF_SET_FILTERING_PARAMETERS = 0x01
|
||||
APCF_BROADCASTER_ADDRESS = 0x02
|
||||
APCF_SERVICE_UUID = 0x03
|
||||
APCF_SERVICE_SOLICITATION_UUID = 0x04
|
||||
APCF_LOCAL_NAME = 0x05
|
||||
APCF_MANUFACTURER_DATA = 0x06
|
||||
APCF_SERVICE_DATA = 0x07
|
||||
APCF_TRANSPORT_DISCOVERY_SERVICE = 0x08
|
||||
APCF_AD_TYPE_FILTER = 0x09
|
||||
APCF_READ_EXTENDED_FEATURES = 0xFF
|
||||
|
||||
OPCODE_NAMES = {
|
||||
APCF_ENABLE: 'APCF_ENABLE',
|
||||
APCF_SET_FILTERING_PARAMETERS: 'APCF_SET_FILTERING_PARAMETERS',
|
||||
APCF_BROADCASTER_ADDRESS: 'APCF_BROADCASTER_ADDRESS',
|
||||
APCF_SERVICE_UUID: 'APCF_SERVICE_UUID',
|
||||
APCF_SERVICE_SOLICITATION_UUID: 'APCF_SERVICE_SOLICITATION_UUID',
|
||||
APCF_LOCAL_NAME: 'APCF_LOCAL_NAME',
|
||||
APCF_MANUFACTURER_DATA: 'APCF_MANUFACTURER_DATA',
|
||||
APCF_SERVICE_DATA: 'APCF_SERVICE_DATA',
|
||||
APCF_TRANSPORT_DISCOVERY_SERVICE: 'APCF_TRANSPORT_DISCOVERY_SERVICE',
|
||||
APCF_AD_TYPE_FILTER: 'APCF_AD_TYPE_FILTER',
|
||||
APCF_READ_EXTENDED_FEATURES: 'APCF_READ_EXTENDED_FEATURES',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def opcode_name(cls, opcode):
|
||||
return name_or_number(cls.OPCODE_NAMES, opcode)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('total_tx_time_ms', 4),
|
||||
('total_rx_time_ms', 4),
|
||||
('total_idle_time_ms', 4),
|
||||
('total_energy_used', 4),
|
||||
],
|
||||
)
|
||||
class HCI_Get_Controller_Activity_Energy_Info_Command(HCI_Command):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_get_controller_activity_energy_info
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
(
|
||||
'opcode',
|
||||
{
|
||||
'size': 1,
|
||||
'mapper': lambda x: HCI_A2DP_Hardware_Offload_Command.opcode_name(x),
|
||||
},
|
||||
),
|
||||
('payload', '*'),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
(
|
||||
'opcode',
|
||||
{
|
||||
'size': 1,
|
||||
'mapper': lambda x: HCI_A2DP_Hardware_Offload_Command.opcode_name(x),
|
||||
},
|
||||
),
|
||||
('payload', '*'),
|
||||
],
|
||||
)
|
||||
class HCI_A2DP_Hardware_Offload_Command(HCI_Command):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#a2dp-hardware-offload-support
|
||||
|
||||
NOTE: the subcommand-specific payloads are left as opaque byte arrays in this
|
||||
implementation. A future enhancement may define subcommand-specific data structures.
|
||||
'''
|
||||
|
||||
# A2DP Hardware Offload Subcommands
|
||||
# TODO: use the OpenIntEnum class (when upcoming PR is merged)
|
||||
START_A2DP_OFFLOAD = 0x01
|
||||
STOP_A2DP_OFFLOAD = 0x02
|
||||
|
||||
OPCODE_NAMES = {
|
||||
START_A2DP_OFFLOAD: 'START_A2DP_OFFLOAD',
|
||||
STOP_A2DP_OFFLOAD: 'STOP_A2DP_OFFLOAD',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def opcode_name(cls, opcode):
|
||||
return name_or_number(cls.OPCODE_NAMES, opcode)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[
|
||||
(
|
||||
'opcode',
|
||||
{
|
||||
'size': 1,
|
||||
'mapper': lambda x: HCI_Dynamic_Audio_Buffer_Command.opcode_name(x),
|
||||
},
|
||||
),
|
||||
('payload', '*'),
|
||||
],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
(
|
||||
'opcode',
|
||||
{
|
||||
'size': 1,
|
||||
'mapper': lambda x: HCI_Dynamic_Audio_Buffer_Command.opcode_name(x),
|
||||
},
|
||||
),
|
||||
('payload', '*'),
|
||||
],
|
||||
)
|
||||
class HCI_Dynamic_Audio_Buffer_Command(HCI_Command):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#dynamic-audio-buffer-command
|
||||
|
||||
NOTE: the subcommand-specific payloads are left as opaque byte arrays in this
|
||||
implementation. A future enhancement may define subcommand-specific data structures.
|
||||
'''
|
||||
|
||||
# Dynamic Audio Buffer Subcommands
|
||||
# TODO: use the OpenIntEnum class (when upcoming PR is merged)
|
||||
GET_AUDIO_BUFFER_TIME_CAPABILITY = 0x01
|
||||
|
||||
OPCODE_NAMES = {
|
||||
GET_AUDIO_BUFFER_TIME_CAPABILITY: 'GET_AUDIO_BUFFER_TIME_CAPABILITY',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def opcode_name(cls, opcode):
|
||||
return name_or_number(cls.OPCODE_NAMES, opcode)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Vendor_Event.event(
|
||||
fields=[
|
||||
('quality_report_id', 1),
|
||||
('packet_types', 1),
|
||||
('connection_handle', 2),
|
||||
('connection_role', {'size': 1, 'mapper': HCI_Constant.role_name}),
|
||||
('tx_power_level', -1),
|
||||
('rssi', -1),
|
||||
('snr', 1),
|
||||
('unused_afh_channel_count', 1),
|
||||
('afh_select_unideal_channel_count', 1),
|
||||
('lsto', 2),
|
||||
('connection_piconet_clock', 4),
|
||||
('retransmission_count', 4),
|
||||
('no_rx_count', 4),
|
||||
('nak_count', 4),
|
||||
('last_tx_ack_timestamp', 4),
|
||||
('flow_off_count', 4),
|
||||
('last_flow_on_timestamp', 4),
|
||||
('buffer_overflow_bytes', 4),
|
||||
('buffer_underflow_bytes', 4),
|
||||
('bdaddr', Address.parse_address),
|
||||
('cal_failed_item_count', 1),
|
||||
('tx_total_packets', 4),
|
||||
('tx_unacked_packets', 4),
|
||||
('tx_flushed_packets', 4),
|
||||
('tx_last_subevent_packets', 4),
|
||||
('crc_error_packets', 4),
|
||||
('rx_duplicate_packets', 4),
|
||||
('vendor_specific_parameters', '*'),
|
||||
]
|
||||
)
|
||||
class HCI_Bluetooth_Quality_Report_Event(HCI_Vendor_Event):
|
||||
# pylint: disable=line-too-long
|
||||
'''
|
||||
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#bluetooth-quality-report-sub-event
|
||||
'''
|
||||
0
bumble/vendor/zephyr/__init__.py
vendored
Normal file
0
bumble/vendor/zephyr/__init__.py
vendored
Normal file
88
bumble/vendor/zephyr/hci.py
vendored
Normal file
88
bumble/vendor/zephyr/hci.py
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
# Copyright 2021-2023 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from bumble.hci import (
|
||||
hci_vendor_command_op_code,
|
||||
HCI_Command,
|
||||
STATUS_SPEC,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Zephyr RTOS Vendor Specific Commands and Events.
|
||||
# Only a subset of the commands are implemented here currently.
|
||||
#
|
||||
# pylint: disable-next=line-too-long
|
||||
# See https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
|
||||
HCI_WRITE_TX_POWER_LEVEL_COMMAND = hci_vendor_command_op_code(0x000E)
|
||||
HCI_READ_TX_POWER_LEVEL_COMMAND = hci_vendor_command_op_code(0x000F)
|
||||
|
||||
HCI_Command.register_commands(globals())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class TX_Power_Level_Command:
|
||||
'''
|
||||
Base class for read and write TX power level HCI commands
|
||||
'''
|
||||
|
||||
TX_POWER_HANDLE_TYPE_ADV = 0x00
|
||||
TX_POWER_HANDLE_TYPE_SCAN = 0x01
|
||||
TX_POWER_HANDLE_TYPE_CONN = 0x02
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[('handle_type', 1), ('connection_handle', 2), ('tx_power_level', -1)],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('handle_type', 1),
|
||||
('connection_handle', 2),
|
||||
('selected_tx_power_level', -1),
|
||||
],
|
||||
)
|
||||
class HCI_Write_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
|
||||
'''
|
||||
Write TX power level. See BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL in
|
||||
https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
|
||||
|
||||
Power level is in dB. Connection handle for TX_POWER_HANDLE_TYPE_ADV and
|
||||
TX_POWER_HANDLE_TYPE_SCAN should be zero.
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Command.command(
|
||||
fields=[('handle_type', 1), ('connection_handle', 2)],
|
||||
return_parameters_fields=[
|
||||
('status', STATUS_SPEC),
|
||||
('handle_type', 1),
|
||||
('connection_handle', 2),
|
||||
('tx_power_level', -1),
|
||||
],
|
||||
)
|
||||
class HCI_Read_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
|
||||
'''
|
||||
Read TX power level. See BT_HCI_OP_VS_READ_TX_POWER_LEVEL in
|
||||
https://github.com/zephyrproject-rtos/zephyr/blob/main/include/zephyr/bluetooth/hci_vs.h
|
||||
|
||||
Power level is in dB. Connection handle for TX_POWER_HANDLE_TYPE_ADV and
|
||||
TX_POWER_HANDLE_TYPE_SCAN should be zero.
|
||||
'''
|
||||
@@ -36,6 +36,9 @@ nav:
|
||||
- HCI Socket: transports/hci_socket.md
|
||||
- Android Emulator: transports/android_emulator.md
|
||||
- File: transports/file.md
|
||||
- Drivers:
|
||||
- Overview: drivers/index.md
|
||||
- Realtek: drivers/realtek.md
|
||||
- API:
|
||||
- Guide: api/guide.md
|
||||
- Examples: api/examples.md
|
||||
@@ -61,8 +64,12 @@ nav:
|
||||
- Linux: platforms/linux.md
|
||||
- Windows: platforms/windows.md
|
||||
- Android: platforms/android.md
|
||||
- Zephyr: platforms/zephyr.md
|
||||
- Examples:
|
||||
- Overview: examples/index.md
|
||||
- Extras:
|
||||
- Overview: extras/index.md
|
||||
- Android Remote HCI: extras/android_remote_hci.md
|
||||
|
||||
copyright: Copyright 2021-2023 Google LLC
|
||||
|
||||
|
||||
BIN
docs/mkdocs/src/downloads/zephyr/hci_usb.zip
Normal file
BIN
docs/mkdocs/src/downloads/zephyr/hci_usb.zip
Normal file
Binary file not shown.
10
docs/mkdocs/src/drivers/index.md
Normal file
10
docs/mkdocs/src/drivers/index.md
Normal file
@@ -0,0 +1,10 @@
|
||||
DRIVERS
|
||||
=======
|
||||
|
||||
Some Bluetooth controllers require a driver to function properly.
|
||||
This may include, for instance, loading a Firmware image or patch,
|
||||
loading a configuration.
|
||||
|
||||
Drivers included in the module are:
|
||||
|
||||
* [Realtek](realtek.md): Loading of Firmware and Config for Realtek USB dongles.
|
||||
62
docs/mkdocs/src/drivers/realtek.md
Normal file
62
docs/mkdocs/src/drivers/realtek.md
Normal file
@@ -0,0 +1,62 @@
|
||||
REALTEK DRIVER
|
||||
==============
|
||||
|
||||
This driver supports loading firmware images and optional config data to
|
||||
USB dongles with a Realtek chipset.
|
||||
A number of USB dongles are supported, but likely not all.
|
||||
When using a USB dongle, the USB product ID and manufacturer ID are used
|
||||
to find whether a matching set of firmware image and config data
|
||||
is needed for that specific model. If a match exists, the driver will try
|
||||
load the firmware image and, if needed, config data.
|
||||
The driver will look for those files by name, in order, in:
|
||||
|
||||
* The directory specified by the environment variable `BUMBLE_RTK_FIRMWARE_DIR`
|
||||
if set.
|
||||
* The directory `<package-dir>/drivers/rtk_fw` where `<package-dir>` is the directory
|
||||
where the `bumble` package is installed.
|
||||
* The current directory.
|
||||
|
||||
|
||||
Obtaining Firmware Images and Config Data
|
||||
-----------------------------------------
|
||||
|
||||
Firmware images and config data may be obtained from a variety of online
|
||||
sources.
|
||||
To facilitate finding a downloading the, the utility program `bumble-rtk-fw-download`
|
||||
may be used.
|
||||
|
||||
```
|
||||
Usage: bumble-rtk-fw-download [OPTIONS]
|
||||
|
||||
Download RTK firmware images and configs.
|
||||
|
||||
Options:
|
||||
--output-dir TEXT Output directory where the files will be
|
||||
saved [default: .]
|
||||
--source [linux-kernel|realtek-opensource|linux-from-scratch]
|
||||
[default: linux-kernel]
|
||||
--single TEXT Only download a single image set, by its
|
||||
base name
|
||||
--force Overwrite files if they already exist
|
||||
--parse Parse the FW image after saving
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
Utility
|
||||
-------
|
||||
|
||||
The `bumble-rtk-util` utility may be used to interact with a Realtek USB dongle
|
||||
and/or firmware images.
|
||||
|
||||
```
|
||||
Usage: bumble-rtk-util [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
drop Drop a firmware image from the USB dongle.
|
||||
info Get the firmware info from a USB dongle.
|
||||
load Load a firmware image into the USB dongle.
|
||||
parse Parse a firmware image.
|
||||
```
|
||||
141
docs/mkdocs/src/extras/android_remote_hci.md
Normal file
141
docs/mkdocs/src/extras/android_remote_hci.md
Normal file
@@ -0,0 +1,141 @@
|
||||
ANDROID REMOTE HCI APP
|
||||
======================
|
||||
|
||||
This application allows using an android phone's built-in Bluetooth controller with
|
||||
a Bumble host stack running outside the phone (typically a development laptop or desktop).
|
||||
The app runs an HCI proxy between a TCP socket on the "outside" and the Bluetooth HCI HAL
|
||||
on the "inside". (See [this page](https://source.android.com/docs/core/connect/bluetooth) for a high level
|
||||
description of the Android Bluetooth HCI HAL).
|
||||
The HCI packets received on the TCP socket are forwarded to the phone's controller, and the
|
||||
packets coming from the controller are forwarded to the TCP socket.
|
||||
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
You can build the app by running `./gradlew build` (use `gradlew.bat` on Windows) from the `RemoteHCI` top level directory.
|
||||
You can also build with Android Studio: open the `RemoteHCI` project. You can build and/or debug from there.
|
||||
|
||||
If the build succeeds, you can find the app APKs (debug and release) at:
|
||||
|
||||
* [Release] ``app/build/outputs/apk/release/app-release-unsigned.apk``
|
||||
* [Debug] ``app/build/outputs/apk/debug/app-debug.apk``
|
||||
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
### Preconditions
|
||||
When the proxy starts (tapping the "Start" button in the app's main activity), it will try to
|
||||
bind to the Bluetooth HAL. This requires disabling SELinux temporarily, and being the only HAL client.
|
||||
|
||||
#### Disabling SELinux
|
||||
Binding to the Bluetooth HCI HAL requires certain SELinux permissions that can't simply be changed
|
||||
on a device without rebuilding its system image. To bypass these restrictions, you will need
|
||||
to disable SELinux on your phone (please be aware that this is global, not just for the proxy app,
|
||||
so proceed with caution).
|
||||
In order to disable SELinux, you need to root the phone (it may be advisable to do this on a
|
||||
development phone).
|
||||
|
||||
!!! tip "Disabling SELinux Temporarily"
|
||||
Restart `adb` as root:
|
||||
```bash
|
||||
$ adb root
|
||||
```
|
||||
|
||||
Then disable SELinux
|
||||
```bash
|
||||
$ adb shell setenforce 0
|
||||
```
|
||||
|
||||
Once you're done using the proxy, you can restore SELinux, if you need to, with
|
||||
```bash
|
||||
$ adb shell setenforce 1
|
||||
```
|
||||
|
||||
This state will also reset to the normal SELinux enforcement when you reboot.
|
||||
|
||||
#### Stopping the bluetooth process
|
||||
Since the Bluetooth HAL service can only accept one client, and that in normal conditions
|
||||
that client is the Android's bluetooth stack, it is required to first shut down the
|
||||
Android bluetooth stack process.
|
||||
|
||||
!!! tip "Checking if the Bluetooth process is running"
|
||||
```bash
|
||||
$ adb shell "ps -A | grep com.google.android.bluetooth"
|
||||
```
|
||||
If the process is running, you will get a line like:
|
||||
```
|
||||
bluetooth 10759 876 17455796 136620 do_epoll_wait 0 S com.google.android.bluetooth
|
||||
```
|
||||
If you don't, it means that the process is not running and you are clear to proceed.
|
||||
|
||||
Simply turning Bluetooth off from the phone's settings does not ensure that the bluetooth process will exit.
|
||||
If the bluetooth process is still running after toggling Bluetooth off from the settings, you may try enabling
|
||||
Airplane Mode, then rebooting. The bluetooth process should, in theory, not restart after the reboot.
|
||||
|
||||
!!! tip "Stopping the bluetooth process with adb"
|
||||
```bash
|
||||
$ adb shell cmd bluetooth_manager disable
|
||||
```
|
||||
|
||||
### Starting the app
|
||||
You can start the app from the Android launcher, from Android Studio, or with `adb`
|
||||
|
||||
#### Launching from the launcher
|
||||
Just tap the app icon on the launcher, check the TCP port that is configured, and tap
|
||||
the "Start" button.
|
||||
|
||||
#### Launching with `adb`
|
||||
Using the `am` command, you can start the activity, and pass it arguments so that you can
|
||||
automatically start the proxy, and/or set the port number.
|
||||
|
||||
!!! tip "Launching from adb with auto-start"
|
||||
```bash
|
||||
$ adb shell am start -n com.github.google.bumble.remotehci/.MainActivity --ez autostart true
|
||||
```
|
||||
|
||||
!!! tip "Launching from adb with auto-start and a port"
|
||||
In this example, we auto-start the proxy upon launch, with the port set to 9995
|
||||
```bash
|
||||
$ adb shell am start -n com.github.google.bumble.remotehci/.MainActivity --ez autostart true --ei port 9995
|
||||
```
|
||||
|
||||
#### Selecting a TCP port
|
||||
The RemoteHCI app's main activity has a "TCP Port" setting where you can change the port on
|
||||
which the proxy is accepting connections. If the default value isn't suitable, you can
|
||||
change it there (you can also use the special value 0 to let the OS assign a port number for you).
|
||||
|
||||
### Connecting to the proxy
|
||||
To connect the Bumble stack to the proxy, you need to be able to reach the phone's network
|
||||
stack. This can be done over the phone's WiFi connection, or, alternatively, using an `adb`
|
||||
TCP forward (which should be faster than over WiFi).
|
||||
|
||||
!!! tip "Forwarding TCP with `adb`"
|
||||
To connect to the proxy via an `adb` TCP forward, use:
|
||||
```bash
|
||||
$ adb forward tcp:<outside-port> tcp:<inside-port>
|
||||
```
|
||||
Where ``<outside-port>`` is the port number for a listening socket on your laptop or
|
||||
desktop machine, and <inside-port> is the TCP port selected in the app's user interface.
|
||||
Those two ports may be the same, of course.
|
||||
For example, with the default TCP port 9993:
|
||||
```bash
|
||||
$ adb forward tcp:9993 tcp:9993
|
||||
```
|
||||
|
||||
Once you've ensured that you can reach the proxy's TCP port on the phone, either directly or
|
||||
via an `adb` forward, you can then use it as a Bumble transport, using the transport name:
|
||||
``tcp-client:<host>:<port>`` syntax.
|
||||
|
||||
!!! example "Connecting a Bumble client"
|
||||
Connecting the `bumble-controller-info` app to the phone's controller.
|
||||
Assuming you have set up an `adb` forward on port 9993:
|
||||
```bash
|
||||
$ bumble-controller-info tcp-client:localhost:9993
|
||||
```
|
||||
|
||||
Or over WiFi with, in this example, the IP address of the phone being ```192.168.86.27```
|
||||
```bash
|
||||
$ bumble-controller-info tcp-client:192.168.86.27:9993
|
||||
```
|
||||
11
docs/mkdocs/src/extras/index.md
Normal file
11
docs/mkdocs/src/extras/index.md
Normal file
@@ -0,0 +1,11 @@
|
||||
EXTRAS
|
||||
======
|
||||
|
||||
A collection of add-ons, apps and tools, to the Bumble project.
|
||||
|
||||
Android Remote HCI
|
||||
------------------
|
||||
|
||||
Allows using an Android phone's built-in Bluetooth controller with a Bumble
|
||||
stack running on a development machine.
|
||||
See [Android Remote HCI](android_remote_hci.md) for details.
|
||||
@@ -3,7 +3,7 @@ HARDWARE
|
||||
|
||||
The Bumble Host connects to a controller over an [HCI Transport](../transports/index.md).
|
||||
To use a hardware controller attached to the host on which the host application is running, the transport is typically either [HCI over UART](../transports/serial.md) or [HCI over USB](../transports/usb.md).
|
||||
On Linux, the [VHCI Transport](../transports/vhci.md) can be used to communicate with any controller hardware managed by the operating system. Alternatively, a remote controller (a phyiscal controller attached to a remote host) can be used by connecting one of the networked transports (such as the [TCP Client transport](../transports/tcp_client.md), the [TCP Server transport](../transports/tcp_server.md) or the [UDP Transport](../transports/udp.md)) to an [HCI Bridge](../apps_and_tools/hci_bridge) bridging the network transport to a physical controller on a remote host.
|
||||
On Linux, the [VHCI Transport](../transports/vhci.md) can be used to communicate with any controller hardware managed by the operating system. Alternatively, a remote controller (a phyiscal controller attached to a remote host) can be used by connecting one of the networked transports (such as the [TCP Client transport](../transports/tcp_client.md), the [TCP Server transport](../transports/tcp_server.md) or the [UDP Transport](../transports/udp.md)) to an [HCI Bridge](../apps_and_tools/hci_bridge.md) bridging the network transport to a physical controller on a remote host.
|
||||
|
||||
In theory, any controller that is compliant with the HCI over UART or HCI over USB protocols can be used.
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ For platform-specific information, see the following pages:
|
||||
* :material-linux: Linux - see the [Linux platform page](linux.md)
|
||||
* :material-microsoft-windows: Windows - see the [Windows platform page](windows.md)
|
||||
* :material-android: Android - see the [Android platform page](android.md)
|
||||
* :material-memory: Zephyr - see the [Zephyr platform page](zephyr.md)
|
||||
|
||||
51
docs/mkdocs/src/platforms/zephyr.md
Normal file
51
docs/mkdocs/src/platforms/zephyr.md
Normal file
@@ -0,0 +1,51 @@
|
||||
:material-memory: ZEPHYR PLATFORM
|
||||
=================================
|
||||
|
||||
Set TX Power on nRF52840
|
||||
------------------------
|
||||
|
||||
The Nordic nRF52840 supports Zephyr's vendor specific HCI command for setting TX
|
||||
power during advertising, connection, or scanning. With the example [HCI
|
||||
USB](https://docs.zephyrproject.org/latest/samples/bluetooth/hci_usb/README.html)
|
||||
application, an [nRF52840
|
||||
dongle](https://www.nordicsemi.com/Products/Development-
|
||||
hardware/nRF52840-Dongle) can be used as a Bumble controller.
|
||||
|
||||
To add dynamic TX power support to the HCI USB application, add the following to
|
||||
`zephyr/samples/bluetooth/hci_usb/prj.conf` and build.
|
||||
|
||||
```
|
||||
CONFIG_BT_CTLR_ADVANCED_FEATURES=y
|
||||
CONFIG_BT_CTLR_CONN_RSSI=y
|
||||
CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y
|
||||
```
|
||||
|
||||
Alternatively, a prebuilt firmware application can be downloaded here:
|
||||
[hci_usb.zip](../downloads/zephyr/hci_usb.zip).
|
||||
|
||||
Put the nRF52840 dongle into bootloader mode by pressing the RESET button. The
|
||||
LED should pulse red. Load the firmware application with the `nrfutil` tool:
|
||||
|
||||
```
|
||||
nrfutil dfu usb-serial -pkg hci_usb.zip -p /dev/ttyACM0
|
||||
```
|
||||
|
||||
The vendor specific HCI commands to read and write TX power are defined in
|
||||
`bumble/vendor/zephyr/hci.py` and may be used as such:
|
||||
|
||||
```python
|
||||
from bumble.vendor.zephyr.hci import HCI_Write_Tx_Power_Level_Command
|
||||
|
||||
# set advertising power to -4 dB
|
||||
response = await host.send_command(
|
||||
HCI_Write_Tx_Power_Level_Command(
|
||||
handle_type=HCI_Write_Tx_Power_Level_Command.TX_POWER_HANDLE_TYPE_ADV,
|
||||
connection_handle=0,
|
||||
tx_power_level=-4,
|
||||
)
|
||||
)
|
||||
|
||||
if response.return_parameters.status == HCI_SUCCESS:
|
||||
print(f"TX power set to {response.return_parameters.selected_tx_power_level}")
|
||||
|
||||
```
|
||||
@@ -1,11 +1,11 @@
|
||||
UDP TRANSPORT
|
||||
=============
|
||||
WEBSOCKET CLIENT TRANSPORT
|
||||
==========================
|
||||
|
||||
The UDP transport is a UDP socket, receiving packets on a specified port number, and sending packets to a specified host and port number.
|
||||
The WebSocket Client transport is WebSocket connection to a WebSocket server over which HCI packets
|
||||
are sent and received.
|
||||
|
||||
## Moniker
|
||||
The moniker syntax for a UDP transport is: `udp:<local-host>:<local-port>,<remote-host>:<remote-port>`.
|
||||
The moniker syntax for a WebSocket Client transport is: `ws-client:<ws-url>`
|
||||
|
||||
!!! example
|
||||
`udp:0.0.0.0:9000,127.0.0.1:9001`
|
||||
UDP transport where packets are received on port `9000` and sent to `127.0.0.1` on port `9001`
|
||||
`ws-client:ws://localhost:1234/some/path`
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
UDP TRANSPORT
|
||||
=============
|
||||
WEBSOCKET SERVER TRANSPORT
|
||||
==========================
|
||||
|
||||
The UDP transport is a UDP socket, receiving packets on a specified port number, and sending packets to a specified host and port number.
|
||||
The WebSocket Server transport is WebSocket server that accepts connections from a WebSocket
|
||||
client. HCI packets are sent and received over the connection.
|
||||
|
||||
## Moniker
|
||||
The moniker syntax for a UDP transport is: `udp:<local-host>:<local-port>,<remote-host>:<remote-port>`.
|
||||
The moniker syntax for a WebSocket Server transport is: `ws-server:<host>:<port>`,
|
||||
where `<host>` may be the address of a local network interface, or `_`to accept connections on all local network interfaces. `<port>` is the TCP port number on which to accept connections.
|
||||
|
||||
|
||||
!!! example
|
||||
`udp:0.0.0.0:9000,127.0.0.1:9001`
|
||||
UDP transport where packets are received on port `9000` and sent to `127.0.0.1` on port `9001`
|
||||
`ws-server:_:9001`
|
||||
|
||||
@@ -3,7 +3,7 @@ channels:
|
||||
- defaults
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- pip=20
|
||||
- pip=23
|
||||
- python=3.8
|
||||
- pip:
|
||||
- --editable .[development,documentation,test]
|
||||
|
||||
@@ -30,7 +30,7 @@ from bumble.core import (
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
)
|
||||
from bumble.rfcomm import Client
|
||||
from bumble import rfcomm, hfp
|
||||
from bumble.sdp import (
|
||||
Client as SDP_Client,
|
||||
DataElement,
|
||||
@@ -39,7 +39,9 @@ from bumble.sdp import (
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
)
|
||||
from bumble.hfp import HfpProtocol
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -181,7 +183,7 @@ async def main():
|
||||
|
||||
# Create a client and start it
|
||||
print('@@@ Starting to RFCOMM client...')
|
||||
rfcomm_client = Client(device, connection)
|
||||
rfcomm_client = rfcomm.Client(device, connection)
|
||||
rfcomm_mux = await rfcomm_client.start()
|
||||
print('@@@ Started')
|
||||
|
||||
@@ -196,7 +198,7 @@ async def main():
|
||||
return
|
||||
|
||||
# Protocol loop (just for testing at this point)
|
||||
protocol = HfpProtocol(session)
|
||||
protocol = hfp.HfpProtocol(session)
|
||||
while True:
|
||||
line = await protocol.next_line()
|
||||
|
||||
|
||||
@@ -21,82 +21,22 @@ import os
|
||||
import logging
|
||||
import json
|
||||
import websockets
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.rfcomm import Server as RfcommServer
|
||||
from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
)
|
||||
from bumble.core import (
|
||||
BT_GENERIC_AUDIO_SERVICE,
|
||||
BT_HANDSFREE_SERVICE,
|
||||
BT_L2CAP_PROTOCOL_ID,
|
||||
BT_RFCOMM_PROTOCOL_ID,
|
||||
)
|
||||
from bumble.hfp import HfpProtocol
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def make_sdp_records(rfcomm_channel):
|
||||
return {
|
||||
0x00010001: [
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(0x00010001),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_HANDSFREE_SERVICE),
|
||||
DataElement.uuid(BT_GENERIC_AUDIO_SERVICE),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence([DataElement.uuid(BT_L2CAP_PROTOCOL_ID)]),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_8(rfcomm_channel),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_HANDSFREE_SERVICE),
|
||||
DataElement.unsigned_integer_16(0x0105),
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
}
|
||||
from bumble import hfp
|
||||
from bumble.hfp import HfProtocol
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class UiServer:
|
||||
protocol = None
|
||||
protocol: Optional[HfProtocol] = None
|
||||
|
||||
async def start(self):
|
||||
# Start a Websocket server to receive events from a web page
|
||||
"""Start a Websocket server to receive events from a web page."""
|
||||
|
||||
async def serve(websocket, _path):
|
||||
while True:
|
||||
try:
|
||||
@@ -107,7 +47,7 @@ class UiServer:
|
||||
message_type = parsed['type']
|
||||
if message_type == 'at_command':
|
||||
if self.protocol is not None:
|
||||
self.protocol.send_command_line(parsed['command'])
|
||||
await self.protocol.execute_command(parsed['command'])
|
||||
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
pass
|
||||
@@ -117,19 +57,11 @@ class UiServer:
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def protocol_loop(protocol):
|
||||
await protocol.initialize_service()
|
||||
|
||||
while True:
|
||||
await (protocol.next_line())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_dlc(dlc):
|
||||
def on_dlc(dlc, configuration: hfp.Configuration):
|
||||
print('*** DLC connected', dlc)
|
||||
protocol = HfpProtocol(dlc)
|
||||
protocol = HfProtocol(dlc, configuration)
|
||||
UiServer.protocol = protocol
|
||||
asyncio.create_task(protocol_loop(protocol))
|
||||
asyncio.create_task(protocol.run())
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -143,6 +75,27 @@ async def main():
|
||||
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
|
||||
# Hands-Free profile configuration.
|
||||
# TODO: load configuration from file.
|
||||
configuration = hfp.Configuration(
|
||||
supported_hf_features=[
|
||||
hfp.HfFeature.THREE_WAY_CALLING,
|
||||
hfp.HfFeature.REMOTE_VOLUME_CONTROL,
|
||||
hfp.HfFeature.ENHANCED_CALL_STATUS,
|
||||
hfp.HfFeature.ENHANCED_CALL_CONTROL,
|
||||
hfp.HfFeature.CODEC_NEGOTIATION,
|
||||
hfp.HfFeature.HF_INDICATORS,
|
||||
hfp.HfFeature.ESCO_S4_SETTINGS_SUPPORTED,
|
||||
],
|
||||
supported_hf_indicators=[
|
||||
hfp.HfIndicator.BATTERY_LEVEL,
|
||||
],
|
||||
supported_audio_codecs=[
|
||||
hfp.AudioCodec.CVSD,
|
||||
hfp.AudioCodec.MSBC,
|
||||
],
|
||||
)
|
||||
|
||||
# Create a device
|
||||
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
|
||||
device.classic_enabled = True
|
||||
@@ -151,11 +104,13 @@ async def main():
|
||||
rfcomm_server = RfcommServer(device)
|
||||
|
||||
# Listen for incoming DLC connections
|
||||
channel_number = rfcomm_server.listen(on_dlc)
|
||||
channel_number = rfcomm_server.listen(lambda dlc: on_dlc(dlc, configuration))
|
||||
print(f'### Listening for connection on channel {channel_number}')
|
||||
|
||||
# Advertise the HFP RFComm channel in the SDP
|
||||
device.sdp_service_records = make_sdp_records(channel_number)
|
||||
device.sdp_service_records = {
|
||||
0x00010001: hfp.sdp_records(0x00010001, channel_number, configuration)
|
||||
}
|
||||
|
||||
# Let's go!
|
||||
await device.power_on()
|
||||
|
||||
@@ -20,83 +20,109 @@ import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
from bumble.core import UUID
|
||||
from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.core import BT_L2CAP_PROTOCOL_ID, BT_RFCOMM_PROTOCOL_ID, UUID
|
||||
from bumble.rfcomm import Server
|
||||
from bumble.sdp import (
|
||||
DataElement,
|
||||
ServiceAttribute,
|
||||
SDP_PUBLIC_BROWSE_ROOT,
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.rfcomm import make_service_sdp_records
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def sdp_records(channel):
|
||||
def sdp_records(channel, uuid):
|
||||
service_record_handle = 0x00010001
|
||||
return {
|
||||
0x00010001: [
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
|
||||
DataElement.unsigned_integer_32(0x00010001),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[DataElement.uuid(UUID('E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'))]
|
||||
),
|
||||
),
|
||||
ServiceAttribute(
|
||||
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.sequence([DataElement.uuid(BT_L2CAP_PROTOCOL_ID)]),
|
||||
DataElement.sequence(
|
||||
[
|
||||
DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
|
||||
DataElement.unsigned_integer_8(channel),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
service_record_handle: make_service_sdp_records(
|
||||
service_record_handle, channel, UUID(uuid)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_dlc(dlc):
|
||||
print('*** DLC connected', dlc)
|
||||
dlc.sink = lambda data: on_rfcomm_data_received(dlc, data)
|
||||
def on_rfcomm_session(rfcomm_session, tcp_server):
|
||||
print('*** RFComm session connected', rfcomm_session)
|
||||
tcp_server.attach_session(rfcomm_session)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_rfcomm_data_received(dlc, data):
|
||||
print(f'<<< Data received: {data.hex()}')
|
||||
try:
|
||||
message = data.decode('utf-8')
|
||||
print(f'<<< Message = {message}')
|
||||
except Exception:
|
||||
pass
|
||||
class TcpServerProtocol(asyncio.Protocol):
|
||||
def __init__(self, server):
|
||||
self.server = server
|
||||
|
||||
# Echo everything back
|
||||
dlc.write(data)
|
||||
def connection_made(self, transport):
|
||||
peer_name = transport.get_extra_info('peer_name')
|
||||
print(f'<<< TCP Server: connection from {peer_name}')
|
||||
if self.server:
|
||||
self.server.tcp_transport = transport
|
||||
else:
|
||||
transport.close()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
print('<<< TCP Server: connection lost')
|
||||
if self.server:
|
||||
self.server.tcp_transport = None
|
||||
|
||||
def data_received(self, data):
|
||||
print(f'<<< TCP Server: data received: {len(data)} bytes - {data.hex()}')
|
||||
if self.server:
|
||||
self.server.tcp_data_received(data)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class TcpServer:
|
||||
def __init__(self, port):
|
||||
self.rfcomm_session = None
|
||||
self.tcp_transport = None
|
||||
AsyncRunner.spawn(self.run(port))
|
||||
|
||||
def attach_session(self, rfcomm_session):
|
||||
if self.rfcomm_session:
|
||||
self.rfcomm_session.sink = None
|
||||
|
||||
self.rfcomm_session = rfcomm_session
|
||||
rfcomm_session.sink = self.rfcomm_data_received
|
||||
|
||||
def rfcomm_data_received(self, data):
|
||||
print(f'<<< RFCOMM Data: {data.hex()}')
|
||||
if self.tcp_transport:
|
||||
self.tcp_transport.write(data)
|
||||
else:
|
||||
print('!!! no TCP connection, dropping data')
|
||||
|
||||
def tcp_data_received(self, data):
|
||||
if self.rfcomm_session:
|
||||
self.rfcomm_session.write(data)
|
||||
else:
|
||||
print('!!! no RFComm session, dropping data')
|
||||
|
||||
async def run(self, port):
|
||||
print(f'$$$ Starting TCP server on port {port}')
|
||||
|
||||
server = await asyncio.get_running_loop().create_server(
|
||||
lambda: TcpServerProtocol(self), '127.0.0.1', port
|
||||
)
|
||||
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def main():
|
||||
if len(sys.argv) < 3:
|
||||
print('Usage: run_rfcomm_server.py <device-config> <transport-spec>')
|
||||
print('example: run_rfcomm_server.py classic2.json usb:04b4:f901')
|
||||
if len(sys.argv) < 4:
|
||||
print(
|
||||
'Usage: run_rfcomm_server.py <device-config> <transport-spec> '
|
||||
'<tcp-port> [<uuid>]'
|
||||
)
|
||||
print('example: run_rfcomm_server.py classic2.json usb:0 8888')
|
||||
return
|
||||
|
||||
tcp_port = int(sys.argv[3])
|
||||
|
||||
if len(sys.argv) >= 5:
|
||||
uuid = sys.argv[4]
|
||||
else:
|
||||
uuid = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
|
||||
print('<<< connected')
|
||||
@@ -105,15 +131,20 @@ async def main():
|
||||
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
|
||||
device.classic_enabled = True
|
||||
|
||||
# Create and register a server
|
||||
# Create a TCP server
|
||||
tcp_server = TcpServer(tcp_port)
|
||||
|
||||
# Create and register an RFComm server
|
||||
rfcomm_server = Server(device)
|
||||
|
||||
# Listen for incoming DLC connections
|
||||
channel_number = rfcomm_server.listen(on_dlc)
|
||||
print(f'### Listening for connection on channel {channel_number}')
|
||||
channel_number = rfcomm_server.listen(
|
||||
lambda session: on_rfcomm_session(session, tcp_server)
|
||||
)
|
||||
print(f'### Listening for RFComm connections on channel {channel_number}')
|
||||
|
||||
# Setup the SDP to advertise this channel
|
||||
device.sdp_service_records = sdp_records(channel_number)
|
||||
device.sdp_service_records = sdp_records(channel_number, uuid)
|
||||
|
||||
# Start the controller
|
||||
await device.power_on()
|
||||
|
||||
@@ -62,7 +62,7 @@ async def main():
|
||||
print(
|
||||
f'>>> {color(advertisement.address, address_color)} '
|
||||
f'[{color(address_type_string, type_color)}]'
|
||||
f'{address_qualifier}:{separator}RSSI:{advertisement.rssi}'
|
||||
f'{address_qualifier}:{separator}RSSI: {advertisement.rssi}'
|
||||
f'{separator}'
|
||||
f'{advertisement.data.to_string(separator)}'
|
||||
)
|
||||
|
||||
10
extras/android/RemoteHCI/.gitignore
vendored
Normal file
10
extras/android/RemoteHCI/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
1
extras/android/RemoteHCI/app/.gitignore
vendored
Normal file
1
extras/android/RemoteHCI/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
73
extras/android/RemoteHCI/app/build.gradle.kts
Normal file
73
extras/android/RemoteHCI/app/build.gradle.kts
Normal file
@@ -0,0 +1,73 @@
|
||||
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.kotlinAndroid)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.github.google.bumble.remotehci"
|
||||
compileSdk = 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.github.google.bumble.remotehci"
|
||||
minSdk = 26
|
||||
targetSdk = 33
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
aidl = false
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.3"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("reflect"))
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.lifecycle.runtime.ktx)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.ui)
|
||||
implementation(libs.ui.graphics)
|
||||
implementation(libs.ui.tooling.preview)
|
||||
implementation(libs.material3)
|
||||
compileOnly(project(":lib"))
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
androidTestImplementation(platform(libs.compose.bom))
|
||||
androidTestImplementation(libs.ui.test.junit4)
|
||||
debugImplementation(libs.ui.tooling)
|
||||
debugImplementation(libs.ui.test.manifest)
|
||||
//compileOnly(files("${project.rootDir.absolutePath}/sdk/framework.jar"))
|
||||
}
|
||||
21
extras/android/RemoteHCI/app/proguard-rules.pro
vendored
Normal file
21
extras/android/RemoteHCI/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
29
extras/android/RemoteHCI/app/src/main/AndroidManifest.xml
Normal file
29
extras/android/RemoteHCI/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RemoteHCI"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.RemoteHCI">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// This file is a snapshot of an AIDL file. Do not edit it manually. There are
|
||||
// two cases:
|
||||
// 1). this is a frozen version file - do not edit this in any case.
|
||||
// 2). this is a 'current' file. If you make a backwards compatible change to
|
||||
// the interface (from the latest frozen version), the build system will
|
||||
// prompt you to update this file with `m <name>-update-api`.
|
||||
//
|
||||
// You must not make a backward incompatible change to any AIDL file built
|
||||
// with the aidl_interface module type with versions property set. The module
|
||||
// type is used to build AIDL files in a way that they can be used across
|
||||
// independently updatable components of the system. If a device is shipped
|
||||
// with such a backward incompatible change, it has a high risk of breaking
|
||||
// later when a module using the interface is updated, e.g., Mainline modules.
|
||||
|
||||
package android.hardware.bluetooth;
|
||||
@VintfStability
|
||||
interface IBluetoothHci {
|
||||
void close();
|
||||
void initialize(in android.hardware.bluetooth.IBluetoothHciCallbacks callback);
|
||||
void sendAclData(in byte[] data);
|
||||
void sendHciCommand(in byte[] command);
|
||||
void sendIsoData(in byte[] data);
|
||||
void sendScoData(in byte[] data);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// This file is a snapshot of an AIDL file. Do not edit it manually. There are
|
||||
// two cases:
|
||||
// 1). this is a frozen version file - do not edit this in any case.
|
||||
// 2). this is a 'current' file. If you make a backwards compatible change to
|
||||
// the interface (from the latest frozen version), the build system will
|
||||
// prompt you to update this file with `m <name>-update-api`.
|
||||
//
|
||||
// You must not make a backward incompatible change to any AIDL file built
|
||||
// with the aidl_interface module type with versions property set. The module
|
||||
// type is used to build AIDL files in a way that they can be used across
|
||||
// independently updatable components of the system. If a device is shipped
|
||||
// with such a backward incompatible change, it has a high risk of breaking
|
||||
// later when a module using the interface is updated, e.g., Mainline modules.
|
||||
|
||||
package android.hardware.bluetooth;
|
||||
@VintfStability
|
||||
interface IBluetoothHciCallbacks {
|
||||
void aclDataReceived(in byte[] data);
|
||||
void hciEventReceived(in byte[] event);
|
||||
void initializationComplete(in android.hardware.bluetooth.Status status);
|
||||
void isoDataReceived(in byte[] data);
|
||||
void scoDataReceived(in byte[] data);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// This file is a snapshot of an AIDL file. Do not edit it manually. There are
|
||||
// two cases:
|
||||
// 1). this is a frozen version file - do not edit this in any case.
|
||||
// 2). this is a 'current' file. If you make a backwards compatible change to
|
||||
// the interface (from the latest frozen version), the build system will
|
||||
// prompt you to update this file with `m <name>-update-api`.
|
||||
//
|
||||
// You must not make a backward incompatible change to any AIDL file built
|
||||
// with the aidl_interface module type with versions property set. The module
|
||||
// type is used to build AIDL files in a way that they can be used across
|
||||
// independently updatable components of the system. If a device is shipped
|
||||
// with such a backward incompatible change, it has a high risk of breaking
|
||||
// later when a module using the interface is updated, e.g., Mainline modules.
|
||||
|
||||
package android.hardware.bluetooth;
|
||||
@Backing(type="int")
|
||||
@VintfStability
|
||||
enum Status {
|
||||
SUCCESS = 0,
|
||||
ALREADY_INITIALIZED = 1,
|
||||
UNABLE_TO_OPEN_INTERFACE = 2,
|
||||
HARDWARE_INITIALIZATION_ERROR = 3,
|
||||
UNKNOWN = 4,
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.hardware.bluetooth@1.1;
|
||||
|
||||
import @1.0::HciPacket;
|
||||
import @1.0::IBluetoothHci;
|
||||
import IBluetoothHciCallbacks;
|
||||
|
||||
/**
|
||||
* The Host Controller Interface (HCI) is the layer defined by the Bluetooth
|
||||
* specification between the software that runs on the host and the Bluetooth
|
||||
* controller chip. This boundary is the natural choice for a Hardware
|
||||
* Abstraction Layer (HAL). Dealing only in HCI packets and events simplifies
|
||||
* the stack and abstracts away power management, initialization, and other
|
||||
* implementation-specific details related to the hardware.
|
||||
*/
|
||||
interface IBluetoothHci extends @1.0::IBluetoothHci {
|
||||
/**
|
||||
* Same as @1.0, but uses 1.1 Callbacks version
|
||||
*/
|
||||
initialize_1_1(@1.1::IBluetoothHciCallbacks callback);
|
||||
|
||||
/**
|
||||
* Send an ISO data packet (as specified in the Bluetooth Core
|
||||
* Specification v5.2) to the Bluetooth controller.
|
||||
* Packets must be processed in order.
|
||||
* @param data HCI data packet to be sent
|
||||
*/
|
||||
sendIsoData(HciPacket data);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.hardware.bluetooth@1.1;
|
||||
|
||||
import @1.0::HciPacket;
|
||||
import @1.0::IBluetoothHciCallbacks;
|
||||
|
||||
/**
|
||||
* The interface from the Bluetooth Controller to the stack.
|
||||
*/
|
||||
interface IBluetoothHciCallbacks extends @1.0::IBluetoothHciCallbacks {
|
||||
/**
|
||||
* Send a ISO data packet form the controller to the host.
|
||||
* @param data the ISO HCI packet to be passed to the host stack
|
||||
*/
|
||||
isoDataReceived(HciPacket data);
|
||||
};
|
||||
BIN
extras/android/RemoteHCI/app/src/main/ic_launcher-playstore.png
Normal file
BIN
extras/android/RemoteHCI/app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* This file is auto-generated. DO NOT MODIFY.
|
||||
*/
|
||||
package android.hardware.bluetooth;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public interface IBluetoothHci extends android.os.IInterface
|
||||
{
|
||||
/** Default implementation for IBluetoothHci. */
|
||||
public static class Default implements android.hardware.bluetooth.IBluetoothHci
|
||||
{
|
||||
@Override public void close() throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override public void initialize(android.hardware.bluetooth.IBluetoothHciCallbacks callback) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override public void sendAclData(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override public void sendHciCommand(byte[] command) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override public void sendIsoData(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override public void sendScoData(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override
|
||||
public android.os.IBinder asBinder() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/** Local-side IPC implementation stub class. */
|
||||
public static abstract class Stub extends android.os.Binder implements android.hardware.bluetooth.IBluetoothHci
|
||||
{
|
||||
/** Construct the stub at attach it to the interface. */
|
||||
public Stub()
|
||||
{
|
||||
//this.markVintfStability();
|
||||
try {
|
||||
Method method = this.getClass().getMethod("markVintfStability", (Class<?>[])null);
|
||||
method.invoke(this);
|
||||
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
this.attachInterface(this, DESCRIPTOR);
|
||||
}
|
||||
/**
|
||||
* Cast an IBinder object into an android.hardware.bluetooth.IBluetoothHci interface,
|
||||
* generating a proxy if needed.
|
||||
*/
|
||||
public static android.hardware.bluetooth.IBluetoothHci asInterface(android.os.IBinder obj)
|
||||
{
|
||||
if ((obj==null)) {
|
||||
return null;
|
||||
}
|
||||
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
|
||||
if (((iin!=null)&&(iin instanceof android.hardware.bluetooth.IBluetoothHci))) {
|
||||
return ((android.hardware.bluetooth.IBluetoothHci)iin);
|
||||
}
|
||||
return new android.hardware.bluetooth.IBluetoothHci.Stub.Proxy(obj);
|
||||
}
|
||||
@Override public android.os.IBinder asBinder()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
|
||||
{
|
||||
java.lang.String descriptor = DESCRIPTOR;
|
||||
if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
|
||||
data.enforceInterface(descriptor);
|
||||
}
|
||||
switch (code)
|
||||
{
|
||||
case INTERFACE_TRANSACTION:
|
||||
{
|
||||
reply.writeString(descriptor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
switch (code)
|
||||
{
|
||||
case TRANSACTION_close:
|
||||
{
|
||||
this.close();
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
case TRANSACTION_initialize:
|
||||
{
|
||||
android.hardware.bluetooth.IBluetoothHciCallbacks _arg0;
|
||||
_arg0 = android.hardware.bluetooth.IBluetoothHciCallbacks.Stub.asInterface(data.readStrongBinder());
|
||||
this.initialize(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
case TRANSACTION_sendAclData:
|
||||
{
|
||||
byte[] _arg0;
|
||||
_arg0 = data.createByteArray();
|
||||
this.sendAclData(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
case TRANSACTION_sendHciCommand:
|
||||
{
|
||||
byte[] _arg0;
|
||||
_arg0 = data.createByteArray();
|
||||
this.sendHciCommand(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
case TRANSACTION_sendIsoData:
|
||||
{
|
||||
byte[] _arg0;
|
||||
_arg0 = data.createByteArray();
|
||||
this.sendIsoData(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
case TRANSACTION_sendScoData:
|
||||
{
|
||||
byte[] _arg0;
|
||||
_arg0 = data.createByteArray();
|
||||
this.sendScoData(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
return super.onTransact(code, data, reply, flags);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static class Proxy implements android.hardware.bluetooth.IBluetoothHci
|
||||
{
|
||||
private android.os.IBinder mRemote;
|
||||
Proxy(android.os.IBinder remote)
|
||||
{
|
||||
mRemote = remote;
|
||||
}
|
||||
@Override public android.os.IBinder asBinder()
|
||||
{
|
||||
return mRemote;
|
||||
}
|
||||
public java.lang.String getInterfaceDescriptor()
|
||||
{
|
||||
return DESCRIPTOR;
|
||||
}
|
||||
@Override public void close() throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_close, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
@Override public void initialize(android.hardware.bluetooth.IBluetoothHciCallbacks callback) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeStrongInterface(callback);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_initialize, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
@Override public void sendAclData(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeByteArray(data);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_sendAclData, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
@Override public void sendHciCommand(byte[] command) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeByteArray(command);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_sendHciCommand, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
@Override public void sendIsoData(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeByteArray(data);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_sendIsoData, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
@Override public void sendScoData(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeByteArray(data);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_sendScoData, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
static final int TRANSACTION_close = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
|
||||
static final int TRANSACTION_initialize = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
|
||||
static final int TRANSACTION_sendAclData = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
|
||||
static final int TRANSACTION_sendHciCommand = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
|
||||
static final int TRANSACTION_sendIsoData = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
|
||||
static final int TRANSACTION_sendScoData = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5);
|
||||
}
|
||||
public static final java.lang.String DESCRIPTOR = "android$hardware$bluetooth$IBluetoothHci".replace('$', '.');
|
||||
public void close() throws android.os.RemoteException;
|
||||
public void initialize(android.hardware.bluetooth.IBluetoothHciCallbacks callback) throws android.os.RemoteException;
|
||||
public void sendAclData(byte[] data) throws android.os.RemoteException;
|
||||
public void sendHciCommand(byte[] command) throws android.os.RemoteException;
|
||||
public void sendIsoData(byte[] data) throws android.os.RemoteException;
|
||||
public void sendScoData(byte[] data) throws android.os.RemoteException;
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* This file is auto-generated. DO NOT MODIFY.
|
||||
*/
|
||||
package android.hardware.bluetooth;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public interface IBluetoothHciCallbacks extends android.os.IInterface
|
||||
{
|
||||
/** Default implementation for IBluetoothHciCallbacks. */
|
||||
public static class Default implements android.hardware.bluetooth.IBluetoothHciCallbacks
|
||||
{
|
||||
@Override public void aclDataReceived(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override public void hciEventReceived(byte[] event) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override public void initializationComplete(int status) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override public void isoDataReceived(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override public void scoDataReceived(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
}
|
||||
@Override
|
||||
public android.os.IBinder asBinder() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/** Local-side IPC implementation stub class. */
|
||||
public static abstract class Stub extends android.os.Binder implements android.hardware.bluetooth.IBluetoothHciCallbacks
|
||||
{
|
||||
/** Construct the stub at attach it to the interface. */
|
||||
public Stub()
|
||||
{
|
||||
//this.markVintfStability();
|
||||
try {
|
||||
Method method = this.getClass().getMethod("markVintfStability", (Class<?>[])null);
|
||||
method.invoke(this);
|
||||
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
this.attachInterface(this, DESCRIPTOR);
|
||||
}
|
||||
/**
|
||||
* Cast an IBinder object into an android.hardware.bluetooth.IBluetoothHciCallbacks interface,
|
||||
* generating a proxy if needed.
|
||||
*/
|
||||
public static android.hardware.bluetooth.IBluetoothHciCallbacks asInterface(android.os.IBinder obj)
|
||||
{
|
||||
if ((obj==null)) {
|
||||
return null;
|
||||
}
|
||||
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
|
||||
if (((iin!=null)&&(iin instanceof android.hardware.bluetooth.IBluetoothHciCallbacks))) {
|
||||
return ((android.hardware.bluetooth.IBluetoothHciCallbacks)iin);
|
||||
}
|
||||
return new android.hardware.bluetooth.IBluetoothHciCallbacks.Stub.Proxy(obj);
|
||||
}
|
||||
@Override public android.os.IBinder asBinder()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
|
||||
{
|
||||
java.lang.String descriptor = DESCRIPTOR;
|
||||
if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
|
||||
data.enforceInterface(descriptor);
|
||||
}
|
||||
switch (code)
|
||||
{
|
||||
case INTERFACE_TRANSACTION:
|
||||
{
|
||||
reply.writeString(descriptor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
switch (code)
|
||||
{
|
||||
case TRANSACTION_aclDataReceived:
|
||||
{
|
||||
byte[] _arg0;
|
||||
_arg0 = data.createByteArray();
|
||||
this.aclDataReceived(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
case TRANSACTION_hciEventReceived:
|
||||
{
|
||||
byte[] _arg0;
|
||||
_arg0 = data.createByteArray();
|
||||
this.hciEventReceived(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
case TRANSACTION_initializationComplete:
|
||||
{
|
||||
int _arg0;
|
||||
_arg0 = data.readInt();
|
||||
this.initializationComplete(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
case TRANSACTION_isoDataReceived:
|
||||
{
|
||||
byte[] _arg0;
|
||||
_arg0 = data.createByteArray();
|
||||
this.isoDataReceived(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
case TRANSACTION_scoDataReceived:
|
||||
{
|
||||
byte[] _arg0;
|
||||
_arg0 = data.createByteArray();
|
||||
this.scoDataReceived(_arg0);
|
||||
reply.writeNoException();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
return super.onTransact(code, data, reply, flags);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static class Proxy implements android.hardware.bluetooth.IBluetoothHciCallbacks
|
||||
{
|
||||
private android.os.IBinder mRemote;
|
||||
Proxy(android.os.IBinder remote)
|
||||
{
|
||||
mRemote = remote;
|
||||
}
|
||||
@Override public android.os.IBinder asBinder()
|
||||
{
|
||||
return mRemote;
|
||||
}
|
||||
public java.lang.String getInterfaceDescriptor()
|
||||
{
|
||||
return DESCRIPTOR;
|
||||
}
|
||||
@Override public void aclDataReceived(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeByteArray(data);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_aclDataReceived, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
@Override public void hciEventReceived(byte[] event) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeByteArray(event);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_hciEventReceived, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
@Override public void initializationComplete(int status) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeInt(status);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_initializationComplete, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
@Override public void isoDataReceived(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeByteArray(data);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_isoDataReceived, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
@Override public void scoDataReceived(byte[] data) throws android.os.RemoteException
|
||||
{
|
||||
android.os.Parcel _data = android.os.Parcel.obtain();
|
||||
android.os.Parcel _reply = android.os.Parcel.obtain();
|
||||
try {
|
||||
_data.writeInterfaceToken(DESCRIPTOR);
|
||||
_data.writeByteArray(data);
|
||||
boolean _status = mRemote.transact(Stub.TRANSACTION_scoDataReceived, _data, _reply, 0);
|
||||
_reply.readException();
|
||||
}
|
||||
finally {
|
||||
_reply.recycle();
|
||||
_data.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
static final int TRANSACTION_aclDataReceived = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
|
||||
static final int TRANSACTION_hciEventReceived = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
|
||||
static final int TRANSACTION_initializationComplete = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
|
||||
static final int TRANSACTION_isoDataReceived = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
|
||||
static final int TRANSACTION_scoDataReceived = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
|
||||
}
|
||||
public static final java.lang.String DESCRIPTOR = "android$hardware$bluetooth$IBluetoothHciCallbacks".replace('$', '.');
|
||||
public void aclDataReceived(byte[] data) throws android.os.RemoteException;
|
||||
public void hciEventReceived(byte[] event) throws android.os.RemoteException;
|
||||
public void initializationComplete(int status) throws android.os.RemoteException;
|
||||
public void isoDataReceived(byte[] data) throws android.os.RemoteException;
|
||||
public void scoDataReceived(byte[] data) throws android.os.RemoteException;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* This file is auto-generated. DO NOT MODIFY.
|
||||
*/
|
||||
package android.hardware.bluetooth;
|
||||
public @interface Status {
|
||||
public static final int SUCCESS = 0;
|
||||
public static final int ALREADY_INITIALIZED = 1;
|
||||
public static final int UNABLE_TO_OPEN_INTERFACE = 2;
|
||||
public static final int HARDWARE_INITIALIZATION_ERROR = 3;
|
||||
public static final int UNKNOWN = 4;
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
package android.hardware.bluetooth.V1_0;
|
||||
|
||||
import android.os.HidlSupport;
|
||||
import android.os.HwBinder;
|
||||
import android.os.IHwBinder;
|
||||
import android.os.HwBlob;
|
||||
import android.os.HwParcel;
|
||||
import android.os.IHwInterface;
|
||||
import android.os.NativeHandle;
|
||||
|
||||
/**
|
||||
* The Host Controller Interface (HCI) is the layer defined by the Bluetooth
|
||||
* specification between the software that runs on the host and the Bluetooth
|
||||
* controller chip. This boundary is the natural choice for a Hardware
|
||||
* Abstraction Layer (HAL). Dealing only in HCI packets and events simplifies
|
||||
* the stack and abstracts away power management, initialization, and other
|
||||
* implementation-specific details related to the hardware.
|
||||
*/
|
||||
public interface IBluetoothHci extends android.internal.hidl.base.V1_0.IBase {
|
||||
/**
|
||||
* Fully-qualified interface name for this interface.
|
||||
*/
|
||||
public static final String kInterfaceName = "android.hardware.bluetooth@1.0::IBluetoothHci";
|
||||
|
||||
/**
|
||||
* Does a checked conversion from a binder to this class.
|
||||
*/
|
||||
/* package private */ static IBluetoothHci asInterface(IHwBinder binder) {
|
||||
if (binder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
IHwInterface iface =
|
||||
binder.queryLocalInterface(kInterfaceName);
|
||||
|
||||
if ((iface != null) && (iface instanceof IBluetoothHci)) {
|
||||
return (IBluetoothHci)iface;
|
||||
}
|
||||
|
||||
IBluetoothHci proxy = new IBluetoothHci.Proxy(binder);
|
||||
|
||||
try {
|
||||
for (String descriptor : proxy.interfaceChain()) {
|
||||
if (descriptor.equals(kInterfaceName)) {
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
} catch (android.os.RemoteException e) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a checked conversion from any interface to this class.
|
||||
*/
|
||||
public static IBluetoothHci castFrom(IHwInterface iface) {
|
||||
return (iface == null) ? null : IBluetoothHci.asInterface(iface.asBinder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwBinder asBinder();
|
||||
|
||||
/**
|
||||
* This will invoke the equivalent of the C++ getService(std::string) if retry is
|
||||
* true or tryGetService(std::string) if retry is false. If the service is
|
||||
* available on the device and retry is true, this will wait for the service to
|
||||
* start.
|
||||
*
|
||||
*/
|
||||
public static IBluetoothHci getService(String serviceName, boolean retry) throws android.os.RemoteException {
|
||||
return IBluetoothHci.asInterface(HwBinder.getService("android.hardware.bluetooth@1.0::IBluetoothHci", serviceName, retry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls getService("default",retry).
|
||||
*/
|
||||
public static IBluetoothHci getService(boolean retry) throws android.os.RemoteException {
|
||||
return getService("default", retry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this will not wait for the interface to come up if it hasn't yet
|
||||
* started. See getService(String,boolean) instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static IBluetoothHci getService(String serviceName) throws android.os.RemoteException {
|
||||
return IBluetoothHci.asInterface(HwBinder.getService("android.hardware.bluetooth@1.0::IBluetoothHci", serviceName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this will not wait for the interface to come up if it hasn't yet
|
||||
* started. See getService(boolean) instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static IBluetoothHci getService() throws android.os.RemoteException {
|
||||
return getService("default");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the underlying HCI interface.
|
||||
*
|
||||
* This method should be used to initialize any hardware interfaces
|
||||
* required to communicate with the Bluetooth hardware in the
|
||||
* device.
|
||||
*
|
||||
* The |oninitializationComplete| callback must be invoked in response
|
||||
* to this function to indicate success before any other function
|
||||
* (sendHciCommand, sendAclData, * sendScoData) is invoked on this
|
||||
* interface.
|
||||
*
|
||||
* @param callback implements IBluetoothHciCallbacks which will
|
||||
* receive callbacks when incoming HCI packets are received
|
||||
* from the controller to be sent to the host.
|
||||
*/
|
||||
void initialize(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks callback)
|
||||
throws android.os.RemoteException;
|
||||
/**
|
||||
* Send an HCI command (as specified in the Bluetooth Specification
|
||||
* V4.2, Vol 2, Part 5, Section 5.4.1) to the Bluetooth controller.
|
||||
* Commands must be executed in order.
|
||||
*
|
||||
* @param command is the HCI command to be sent
|
||||
*/
|
||||
void sendHciCommand(java.util.ArrayList<Byte> command)
|
||||
throws android.os.RemoteException;
|
||||
/**
|
||||
* Send an HCI ACL data packet (as specified in the Bluetooth Specification
|
||||
* V4.2, Vol 2, Part 5, Section 5.4.2) to the Bluetooth controller.
|
||||
* Packets must be processed in order.
|
||||
* @param data HCI data packet to be sent
|
||||
*/
|
||||
void sendAclData(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException;
|
||||
/**
|
||||
* Send an SCO data packet (as specified in the Bluetooth Specification
|
||||
* V4.2, Vol 2, Part 5, Section 5.4.3) to the Bluetooth controller.
|
||||
* Packets must be processed in order.
|
||||
* @param data HCI data packet to be sent
|
||||
*/
|
||||
void sendScoData(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException;
|
||||
/**
|
||||
* Close the HCI interface
|
||||
*/
|
||||
void close()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides run-time type information for this object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceChain on an IChild object must yield the following:
|
||||
* ["android.hardware.foo@1.0::IChild",
|
||||
* "android.hardware.foo@1.0::IParent"
|
||||
* "android.internal.hidl.base@1.0::IBase"]
|
||||
*
|
||||
* @return descriptors a vector of descriptors of the run-time type of the
|
||||
* object.
|
||||
*/
|
||||
java.util.ArrayList<String> interfaceChain()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Emit diagnostic information to the given file.
|
||||
*
|
||||
* Optionally overriden.
|
||||
*
|
||||
* @param fd File descriptor to dump data to.
|
||||
* Must only be used for the duration of this call.
|
||||
* @param options Arguments for debugging.
|
||||
* Must support empty for default debug information.
|
||||
*/
|
||||
void debug(NativeHandle fd, java.util.ArrayList<String> options)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides run-time type information for this object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceDescriptor on an IChild object must yield
|
||||
* "android.hardware.foo@1.0::IChild"
|
||||
*
|
||||
* @return descriptor a descriptor of the run-time type of the
|
||||
* object (the first element of the vector returned by
|
||||
* interfaceChain())
|
||||
*/
|
||||
String interfaceDescriptor()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Returns hashes of the source HAL files that define the interfaces of the
|
||||
* runtime type information on the object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceChain on an IChild object must yield the following:
|
||||
* [(hash of IChild.hal),
|
||||
* (hash of IParent.hal)
|
||||
* (hash of IBase.hal)].
|
||||
*
|
||||
* SHA-256 is used as the hashing algorithm. Each hash has 32 bytes
|
||||
* according to SHA-256 standard.
|
||||
*
|
||||
* @return hashchain a vector of SHA-1 digests
|
||||
*/
|
||||
java.util.ArrayList<byte[/* 32 */]> getHashChain()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* This method trigger the interface to enable/disable instrumentation based
|
||||
* on system property hal.instrumentation.enable.
|
||||
*/
|
||||
void setHALInstrumentation()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Registers a death recipient, to be called when the process hosting this
|
||||
* interface dies.
|
||||
*
|
||||
* @param recipient a hidl_death_recipient callback object
|
||||
* @param cookie a cookie that must be returned with the callback
|
||||
* @return success whether the death recipient was registered successfully.
|
||||
*/
|
||||
boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides way to determine if interface is running without requesting
|
||||
* any functionality.
|
||||
*/
|
||||
void ping()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Get debug information on references on this interface.
|
||||
* @return info debugging information. See comments of DebugInfo.
|
||||
*/
|
||||
android.internal.hidl.base.V1_0.DebugInfo getDebugInfo()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* This method notifies the interface that one or more system properties
|
||||
* have changed. The default implementation calls
|
||||
* (C++) report_sysprop_change() in libcutils or
|
||||
* (Java) android.os.SystemProperties.reportSyspropChanged,
|
||||
* which in turn calls a set of registered callbacks (eg to update trace
|
||||
* tags).
|
||||
*/
|
||||
void notifySyspropsChanged()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Unregisters the registered death recipient. If this service was registered
|
||||
* multiple times with the same exact death recipient, this unlinks the most
|
||||
* recently registered one.
|
||||
*
|
||||
* @param recipient a previously registered hidl_death_recipient callback
|
||||
* @return success whether the death recipient was unregistered successfully.
|
||||
*/
|
||||
boolean unlinkToDeath(IHwBinder.DeathRecipient recipient)
|
||||
throws android.os.RemoteException;
|
||||
|
||||
public static final class Proxy implements IBluetoothHci {
|
||||
private IHwBinder mRemote;
|
||||
|
||||
public Proxy(IHwBinder remote) {
|
||||
mRemote = java.util.Objects.requireNonNull(remote);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwBinder asBinder() {
|
||||
return mRemote;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
try {
|
||||
return this.interfaceDescriptor() + "@Proxy";
|
||||
} catch (android.os.RemoteException ex) {
|
||||
/* ignored; handled below. */
|
||||
}
|
||||
return "[class or subclass of " + IBluetoothHci.kInterfaceName + "]@Proxy";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(java.lang.Object other) {
|
||||
return HidlSupport.interfacesEqual(this, other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int hashCode() {
|
||||
return this.asBinder().hashCode();
|
||||
}
|
||||
|
||||
// Methods from ::android::hardware::bluetooth::V1_0::IBluetoothHci follow.
|
||||
@Override
|
||||
public void initialize(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks callback)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeStrongBinder(callback == null ? null : callback.asBinder());
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(1 /* initialize */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendHciCommand(java.util.ArrayList<Byte> command)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(command);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(2 /* sendHciCommand */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendAclData(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(3 /* sendAclData */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendScoData(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(4 /* sendScoData */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(5 /* close */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Methods from ::android::hidl::base::V1_0::IBase follow.
|
||||
@Override
|
||||
public java.util.ArrayList<String> interfaceChain()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256067662 /* interfaceChain */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
java.util.ArrayList<String> _hidl_out_descriptors = _hidl_reply.readStringVector();
|
||||
return _hidl_out_descriptors;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(NativeHandle fd, java.util.ArrayList<String> options)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
_hidl_request.writeNativeHandle(fd);
|
||||
_hidl_request.writeStringVector(options);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256131655 /* debug */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String interfaceDescriptor()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256136003 /* interfaceDescriptor */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
String _hidl_out_descriptor = _hidl_reply.readString();
|
||||
return _hidl_out_descriptor;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.ArrayList<byte[/* 32 */]> getHashChain()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256398152 /* getHashChain */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
java.util.ArrayList<byte[/* 32 */]> _hidl_out_hashchain = new java.util.ArrayList<byte[/* 32 */]>();
|
||||
{
|
||||
HwBlob _hidl_blob = _hidl_reply.readBuffer(16 /* size */);
|
||||
{
|
||||
int _hidl_vec_size = _hidl_blob.getInt32(0 /* offset */ + 8 /* offsetof(hidl_vec<T>, mSize) */);
|
||||
HwBlob childBlob = _hidl_reply.readEmbeddedBuffer(
|
||||
_hidl_vec_size * 32,_hidl_blob.handle(),
|
||||
0 /* offset */ + 0 /* offsetof(hidl_vec<T>, mBuffer) */,true /* nullable */);
|
||||
|
||||
((java.util.ArrayList<byte[/* 32 */]>) _hidl_out_hashchain).clear();
|
||||
for (int _hidl_index_0 = 0; _hidl_index_0 < _hidl_vec_size; ++_hidl_index_0) {
|
||||
byte[/* 32 */] _hidl_vec_element = new byte[32];
|
||||
{
|
||||
long _hidl_array_offset_1 = _hidl_index_0 * 32;
|
||||
childBlob.copyToInt8Array(_hidl_array_offset_1, (byte[/* 32 */]) _hidl_vec_element, 32 /* size */);
|
||||
_hidl_array_offset_1 += 32 * 1;
|
||||
}
|
||||
((java.util.ArrayList<byte[/* 32 */]>) _hidl_out_hashchain).add(_hidl_vec_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _hidl_out_hashchain;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHALInstrumentation()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256462420 /* setHALInstrumentation */, _hidl_request, _hidl_reply, 1 /* oneway */);
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie)
|
||||
throws android.os.RemoteException {
|
||||
return mRemote.linkToDeath(recipient, cookie);
|
||||
}
|
||||
@Override
|
||||
public void ping()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256921159 /* ping */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.internal.hidl.base.V1_0.DebugInfo getDebugInfo()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(257049926 /* getDebugInfo */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
android.internal.hidl.base.V1_0.DebugInfo _hidl_out_info = new android.internal.hidl.base.V1_0.DebugInfo();
|
||||
((android.internal.hidl.base.V1_0.DebugInfo) _hidl_out_info).readFromParcel(_hidl_reply);
|
||||
return _hidl_out_info;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifySyspropsChanged()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(257120595 /* notifySyspropsChanged */, _hidl_request, _hidl_reply, 1 /* oneway */);
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unlinkToDeath(IHwBinder.DeathRecipient recipient)
|
||||
throws android.os.RemoteException {
|
||||
return mRemote.unlinkToDeath(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Stub extends HwBinder implements IBluetoothHci {
|
||||
@Override
|
||||
public IHwBinder asBinder() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final java.util.ArrayList<String> interfaceChain() {
|
||||
return new java.util.ArrayList<String>(java.util.Arrays.asList(
|
||||
android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName,
|
||||
android.internal.hidl.base.V1_0.IBase.kInterfaceName));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(NativeHandle fd, java.util.ArrayList<String> options) {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String interfaceDescriptor() {
|
||||
return android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final java.util.ArrayList<byte[/* 32 */]> getHashChain() {
|
||||
return new java.util.ArrayList<byte[/* 32 */]>(java.util.Arrays.asList(
|
||||
new byte[/* 32 */]{52,124,-25,70,-127,86,7,86,127,95,59,83,-28,-128,9,-104,-54,90,-71,53,81,65,-16,-120,15,-64,-49,12,31,-59,-61,85} /* 347ce746815607567f5f3b53e4800998ca5ab9355141f0880fc0cf0c1fc5c355 */,
|
||||
new byte[/* 32 */]{-20,127,-41,-98,-48,45,-6,-123,-68,73,-108,38,-83,-82,62,-66,35,-17,5,36,-13,-51,105,87,19,-109,36,-72,59,24,-54,76} /* ec7fd79ed02dfa85bc499426adae3ebe23ef0524f3cd6957139324b83b18ca4c */));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setHALInstrumentation() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie) {
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void ping() {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final android.internal.hidl.base.V1_0.DebugInfo getDebugInfo() {
|
||||
android.internal.hidl.base.V1_0.DebugInfo info = new android.internal.hidl.base.V1_0.DebugInfo();
|
||||
info.pid = HidlSupport.getPidIfSharable();
|
||||
info.ptr = 0;
|
||||
info.arch = android.internal.hidl.base.V1_0.DebugInfo.Architecture.UNKNOWN;
|
||||
return info;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void notifySyspropsChanged() {
|
||||
HwBinder.enableInstrumentation();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean unlinkToDeath(IHwBinder.DeathRecipient recipient) {
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwInterface queryLocalInterface(String descriptor) {
|
||||
if (kInterfaceName.equals(descriptor)) {
|
||||
return this;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void registerAsService(String serviceName) throws android.os.RemoteException {
|
||||
registerService(serviceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.interfaceDescriptor() + "@Stub";
|
||||
}
|
||||
|
||||
//@Override
|
||||
public void onTransact(int _hidl_code, HwParcel _hidl_request, final HwParcel _hidl_reply, int _hidl_flags)
|
||||
throws android.os.RemoteException {
|
||||
switch (_hidl_code) {
|
||||
case 1 /* initialize */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks callback = android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.asInterface(_hidl_request.readStrongBinder());
|
||||
initialize(callback);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 2 /* sendHciCommand */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> command = _hidl_request.readInt8Vector();
|
||||
sendHciCommand(command);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 3 /* sendAclData */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
sendAclData(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 4 /* sendScoData */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
sendScoData(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 5 /* close */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
close();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256067662 /* interfaceChain */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<String> _hidl_out_descriptors = interfaceChain();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.writeStringVector(_hidl_out_descriptors);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256131655 /* debug */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
NativeHandle fd = _hidl_request.readNativeHandle();
|
||||
java.util.ArrayList<String> options = _hidl_request.readStringVector();
|
||||
debug(fd, options);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256136003 /* interfaceDescriptor */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
String _hidl_out_descriptor = interfaceDescriptor();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.writeString(_hidl_out_descriptor);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256398152 /* getHashChain */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<byte[/* 32 */]> _hidl_out_hashchain = getHashChain();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
{
|
||||
HwBlob _hidl_blob = new HwBlob(16 /* size */);
|
||||
{
|
||||
int _hidl_vec_size = _hidl_out_hashchain.size();
|
||||
_hidl_blob.putInt32(0 /* offset */ + 8 /* offsetof(hidl_vec<T>, mSize) */, _hidl_vec_size);
|
||||
_hidl_blob.putBool(0 /* offset */ + 12 /* offsetof(hidl_vec<T>, mOwnsBuffer) */, false);
|
||||
HwBlob childBlob = new HwBlob((int)(_hidl_vec_size * 32));
|
||||
for (int _hidl_index_0 = 0; _hidl_index_0 < _hidl_vec_size; ++_hidl_index_0) {
|
||||
{
|
||||
long _hidl_array_offset_1 = _hidl_index_0 * 32;
|
||||
byte[] _hidl_array_item_1 = (byte[/* 32 */]) _hidl_out_hashchain.get(_hidl_index_0);
|
||||
|
||||
if (_hidl_array_item_1 == null || _hidl_array_item_1.length != 32) {
|
||||
throw new IllegalArgumentException("Array element is not of the expected length");
|
||||
}
|
||||
|
||||
childBlob.putInt8Array(_hidl_array_offset_1, _hidl_array_item_1);
|
||||
_hidl_array_offset_1 += 32 * 1;
|
||||
}
|
||||
}
|
||||
_hidl_blob.putBlob(0 /* offset */ + 0 /* offsetof(hidl_vec<T>, mBuffer) */, childBlob);
|
||||
}
|
||||
_hidl_reply.writeBuffer(_hidl_blob);
|
||||
}
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256462420 /* setHALInstrumentation */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
setHALInstrumentation();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256660548 /* linkToDeath */:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
case 256921159 /* ping */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
ping();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257049926 /* getDebugInfo */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
android.internal.hidl.base.V1_0.DebugInfo _hidl_out_info = getDebugInfo();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
((android.internal.hidl.base.V1_0.DebugInfo) _hidl_out_info).writeToParcel(_hidl_reply);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257120595 /* notifySyspropsChanged */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
notifySyspropsChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257250372 /* unlinkToDeath */:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,762 @@
|
||||
package android.hardware.bluetooth.V1_0;
|
||||
|
||||
import android.os.HidlSupport;
|
||||
import android.os.HwBinder;
|
||||
import android.os.IHwBinder;
|
||||
import android.os.HwBlob;
|
||||
import android.os.HwParcel;
|
||||
import android.os.IHwInterface;
|
||||
import android.os.NativeHandle;
|
||||
|
||||
/**
|
||||
* The interface from the Bluetooth Controller to the stack.
|
||||
*/
|
||||
public interface IBluetoothHciCallbacks extends android.internal.hidl.base.V1_0.IBase {
|
||||
/**
|
||||
* Fully-qualified interface name for this interface.
|
||||
*/
|
||||
public static final String kInterfaceName = "android.hardware.bluetooth@1.0::IBluetoothHciCallbacks";
|
||||
|
||||
/**
|
||||
* Does a checked conversion from a binder to this class.
|
||||
*/
|
||||
/* package private */ static IBluetoothHciCallbacks asInterface(IHwBinder binder) {
|
||||
if (binder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
IHwInterface iface =
|
||||
binder.queryLocalInterface(kInterfaceName);
|
||||
|
||||
if ((iface != null) && (iface instanceof IBluetoothHciCallbacks)) {
|
||||
return (IBluetoothHciCallbacks)iface;
|
||||
}
|
||||
|
||||
IBluetoothHciCallbacks proxy = new IBluetoothHciCallbacks.Proxy(binder);
|
||||
|
||||
try {
|
||||
for (String descriptor : proxy.interfaceChain()) {
|
||||
if (descriptor.equals(kInterfaceName)) {
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
} catch (android.os.RemoteException e) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a checked conversion from any interface to this class.
|
||||
*/
|
||||
public static IBluetoothHciCallbacks castFrom(IHwInterface iface) {
|
||||
return (iface == null) ? null : IBluetoothHciCallbacks.asInterface(iface.asBinder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwBinder asBinder();
|
||||
|
||||
/**
|
||||
* This will invoke the equivalent of the C++ getService(std::string) if retry is
|
||||
* true or tryGetService(std::string) if retry is false. If the service is
|
||||
* available on the device and retry is true, this will wait for the service to
|
||||
* start.
|
||||
*
|
||||
*/
|
||||
public static IBluetoothHciCallbacks getService(String serviceName, boolean retry) throws android.os.RemoteException {
|
||||
return IBluetoothHciCallbacks.asInterface(HwBinder.getService("android.hardware.bluetooth@1.0::IBluetoothHciCallbacks", serviceName, retry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls getService("default",retry).
|
||||
*/
|
||||
public static IBluetoothHciCallbacks getService(boolean retry) throws android.os.RemoteException {
|
||||
return getService("default", retry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this will not wait for the interface to come up if it hasn't yet
|
||||
* started. See getService(String,boolean) instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static IBluetoothHciCallbacks getService(String serviceName) throws android.os.RemoteException {
|
||||
return IBluetoothHciCallbacks.asInterface(HwBinder.getService("android.hardware.bluetooth@1.0::IBluetoothHciCallbacks", serviceName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this will not wait for the interface to come up if it hasn't yet
|
||||
* started. See getService(boolean) instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static IBluetoothHciCallbacks getService() throws android.os.RemoteException {
|
||||
return getService("default");
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the Bluetooth controller initialization has been
|
||||
* completed.
|
||||
*/
|
||||
void initializationComplete(int status)
|
||||
throws android.os.RemoteException;
|
||||
/**
|
||||
* This function is invoked when an HCI event is received from the
|
||||
* Bluetooth controller to be forwarded to the Bluetooth stack.
|
||||
* @param event is the HCI event to be sent to the Bluetooth stack.
|
||||
*/
|
||||
void hciEventReceived(java.util.ArrayList<Byte> event)
|
||||
throws android.os.RemoteException;
|
||||
/**
|
||||
* Send an ACL data packet form the controller to the host.
|
||||
* @param data the ACL HCI packet to be passed to the host stack
|
||||
*/
|
||||
void aclDataReceived(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException;
|
||||
/**
|
||||
* Send a SCO data packet form the controller to the host.
|
||||
* @param data the SCO HCI packet to be passed to the host stack
|
||||
*/
|
||||
void scoDataReceived(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides run-time type information for this object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceChain on an IChild object must yield the following:
|
||||
* ["android.hardware.foo@1.0::IChild",
|
||||
* "android.hardware.foo@1.0::IParent"
|
||||
* "android.internal.hidl.base@1.0::IBase"]
|
||||
*
|
||||
* @return descriptors a vector of descriptors of the run-time type of the
|
||||
* object.
|
||||
*/
|
||||
java.util.ArrayList<String> interfaceChain()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Emit diagnostic information to the given file.
|
||||
*
|
||||
* Optionally overriden.
|
||||
*
|
||||
* @param fd File descriptor to dump data to.
|
||||
* Must only be used for the duration of this call.
|
||||
* @param options Arguments for debugging.
|
||||
* Must support empty for default debug information.
|
||||
*/
|
||||
void debug(NativeHandle fd, java.util.ArrayList<String> options)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides run-time type information for this object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceDescriptor on an IChild object must yield
|
||||
* "android.hardware.foo@1.0::IChild"
|
||||
*
|
||||
* @return descriptor a descriptor of the run-time type of the
|
||||
* object (the first element of the vector returned by
|
||||
* interfaceChain())
|
||||
*/
|
||||
String interfaceDescriptor()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Returns hashes of the source HAL files that define the interfaces of the
|
||||
* runtime type information on the object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceChain on an IChild object must yield the following:
|
||||
* [(hash of IChild.hal),
|
||||
* (hash of IParent.hal)
|
||||
* (hash of IBase.hal)].
|
||||
*
|
||||
* SHA-256 is used as the hashing algorithm. Each hash has 32 bytes
|
||||
* according to SHA-256 standard.
|
||||
*
|
||||
* @return hashchain a vector of SHA-1 digests
|
||||
*/
|
||||
java.util.ArrayList<byte[/* 32 */]> getHashChain()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* This method trigger the interface to enable/disable instrumentation based
|
||||
* on system property hal.instrumentation.enable.
|
||||
*/
|
||||
void setHALInstrumentation()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Registers a death recipient, to be called when the process hosting this
|
||||
* interface dies.
|
||||
*
|
||||
* @param recipient a hidl_death_recipient callback object
|
||||
* @param cookie a cookie that must be returned with the callback
|
||||
* @return success whether the death recipient was registered successfully.
|
||||
*/
|
||||
boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides way to determine if interface is running without requesting
|
||||
* any functionality.
|
||||
*/
|
||||
void ping()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Get debug information on references on this interface.
|
||||
* @return info debugging information. See comments of DebugInfo.
|
||||
*/
|
||||
android.internal.hidl.base.V1_0.DebugInfo getDebugInfo()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* This method notifies the interface that one or more system properties
|
||||
* have changed. The default implementation calls
|
||||
* (C++) report_sysprop_change() in libcutils or
|
||||
* (Java) android.os.SystemProperties.reportSyspropChanged,
|
||||
* which in turn calls a set of registered callbacks (eg to update trace
|
||||
* tags).
|
||||
*/
|
||||
void notifySyspropsChanged()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Unregisters the registered death recipient. If this service was registered
|
||||
* multiple times with the same exact death recipient, this unlinks the most
|
||||
* recently registered one.
|
||||
*
|
||||
* @param recipient a previously registered hidl_death_recipient callback
|
||||
* @return success whether the death recipient was unregistered successfully.
|
||||
*/
|
||||
boolean unlinkToDeath(IHwBinder.DeathRecipient recipient)
|
||||
throws android.os.RemoteException;
|
||||
|
||||
public static final class Proxy implements IBluetoothHciCallbacks {
|
||||
private IHwBinder mRemote;
|
||||
|
||||
public Proxy(IHwBinder remote) {
|
||||
mRemote = java.util.Objects.requireNonNull(remote);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwBinder asBinder() {
|
||||
return mRemote;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
try {
|
||||
return this.interfaceDescriptor() + "@Proxy";
|
||||
} catch (android.os.RemoteException ex) {
|
||||
/* ignored; handled below. */
|
||||
}
|
||||
return "[class or subclass of " + IBluetoothHciCallbacks.kInterfaceName + "]@Proxy";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(java.lang.Object other) {
|
||||
return HidlSupport.interfacesEqual(this, other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int hashCode() {
|
||||
return this.asBinder().hashCode();
|
||||
}
|
||||
|
||||
// Methods from ::android::hardware::bluetooth::V1_0::IBluetoothHciCallbacks follow.
|
||||
@Override
|
||||
public void initializationComplete(int status)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
_hidl_request.writeInt32(status);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(1 /* initializationComplete */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hciEventReceived(java.util.ArrayList<Byte> event)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(event);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(2 /* hciEventReceived */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void aclDataReceived(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(3 /* aclDataReceived */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scoDataReceived(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(4 /* scoDataReceived */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Methods from ::android::hidl::base::V1_0::IBase follow.
|
||||
@Override
|
||||
public java.util.ArrayList<String> interfaceChain()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256067662 /* interfaceChain */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
java.util.ArrayList<String> _hidl_out_descriptors = _hidl_reply.readStringVector();
|
||||
return _hidl_out_descriptors;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(NativeHandle fd, java.util.ArrayList<String> options)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
_hidl_request.writeNativeHandle(fd);
|
||||
_hidl_request.writeStringVector(options);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256131655 /* debug */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String interfaceDescriptor()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256136003 /* interfaceDescriptor */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
String _hidl_out_descriptor = _hidl_reply.readString();
|
||||
return _hidl_out_descriptor;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.ArrayList<byte[/* 32 */]> getHashChain()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256398152 /* getHashChain */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
java.util.ArrayList<byte[/* 32 */]> _hidl_out_hashchain = new java.util.ArrayList<byte[/* 32 */]>();
|
||||
{
|
||||
HwBlob _hidl_blob = _hidl_reply.readBuffer(16 /* size */);
|
||||
{
|
||||
int _hidl_vec_size = _hidl_blob.getInt32(0 /* offset */ + 8 /* offsetof(hidl_vec<T>, mSize) */);
|
||||
HwBlob childBlob = _hidl_reply.readEmbeddedBuffer(
|
||||
_hidl_vec_size * 32,_hidl_blob.handle(),
|
||||
0 /* offset */ + 0 /* offsetof(hidl_vec<T>, mBuffer) */,true /* nullable */);
|
||||
|
||||
((java.util.ArrayList<byte[/* 32 */]>) _hidl_out_hashchain).clear();
|
||||
for (int _hidl_index_0 = 0; _hidl_index_0 < _hidl_vec_size; ++_hidl_index_0) {
|
||||
byte[/* 32 */] _hidl_vec_element = new byte[32];
|
||||
{
|
||||
long _hidl_array_offset_1 = _hidl_index_0 * 32;
|
||||
childBlob.copyToInt8Array(_hidl_array_offset_1, (byte[/* 32 */]) _hidl_vec_element, 32 /* size */);
|
||||
_hidl_array_offset_1 += 32 * 1;
|
||||
}
|
||||
((java.util.ArrayList<byte[/* 32 */]>) _hidl_out_hashchain).add(_hidl_vec_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _hidl_out_hashchain;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHALInstrumentation()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256462420 /* setHALInstrumentation */, _hidl_request, _hidl_reply, 1 /* oneway */);
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie)
|
||||
throws android.os.RemoteException {
|
||||
return mRemote.linkToDeath(recipient, cookie);
|
||||
}
|
||||
@Override
|
||||
public void ping()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256921159 /* ping */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.internal.hidl.base.V1_0.DebugInfo getDebugInfo()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(257049926 /* getDebugInfo */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
android.internal.hidl.base.V1_0.DebugInfo _hidl_out_info = new android.internal.hidl.base.V1_0.DebugInfo();
|
||||
((android.internal.hidl.base.V1_0.DebugInfo) _hidl_out_info).readFromParcel(_hidl_reply);
|
||||
return _hidl_out_info;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifySyspropsChanged()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(257120595 /* notifySyspropsChanged */, _hidl_request, _hidl_reply, 1 /* oneway */);
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unlinkToDeath(IHwBinder.DeathRecipient recipient)
|
||||
throws android.os.RemoteException {
|
||||
return mRemote.unlinkToDeath(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Stub extends HwBinder implements IBluetoothHciCallbacks {
|
||||
@Override
|
||||
public IHwBinder asBinder() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final java.util.ArrayList<String> interfaceChain() {
|
||||
return new java.util.ArrayList<String>(java.util.Arrays.asList(
|
||||
android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName,
|
||||
android.internal.hidl.base.V1_0.IBase.kInterfaceName));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(NativeHandle fd, java.util.ArrayList<String> options) {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String interfaceDescriptor() {
|
||||
return android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final java.util.ArrayList<byte[/* 32 */]> getHashChain() {
|
||||
return new java.util.ArrayList<byte[/* 32 */]>(java.util.Arrays.asList(
|
||||
new byte[/* 32 */]{-125,95,65,-66,34,-127,-65,-78,47,62,51,-58,-6,-121,11,-34,123,-62,30,55,-27,-49,-70,-7,-93,111,-1,23,6,50,-9,84} /* 835f41be2281bfb22f3e33c6fa870bde7bc21e37e5cfbaf9a36fff170632f754 */,
|
||||
new byte[/* 32 */]{-20,127,-41,-98,-48,45,-6,-123,-68,73,-108,38,-83,-82,62,-66,35,-17,5,36,-13,-51,105,87,19,-109,36,-72,59,24,-54,76} /* ec7fd79ed02dfa85bc499426adae3ebe23ef0524f3cd6957139324b83b18ca4c */));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setHALInstrumentation() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie) {
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void ping() {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final android.internal.hidl.base.V1_0.DebugInfo getDebugInfo() {
|
||||
android.internal.hidl.base.V1_0.DebugInfo info = new android.internal.hidl.base.V1_0.DebugInfo();
|
||||
info.pid = HidlSupport.getPidIfSharable();
|
||||
info.ptr = 0;
|
||||
info.arch = android.internal.hidl.base.V1_0.DebugInfo.Architecture.UNKNOWN;
|
||||
return info;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void notifySyspropsChanged() {
|
||||
HwBinder.enableInstrumentation();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean unlinkToDeath(IHwBinder.DeathRecipient recipient) {
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwInterface queryLocalInterface(String descriptor) {
|
||||
if (kInterfaceName.equals(descriptor)) {
|
||||
return this;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void registerAsService(String serviceName) throws android.os.RemoteException {
|
||||
registerService(serviceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.interfaceDescriptor() + "@Stub";
|
||||
}
|
||||
|
||||
//@Override
|
||||
public void onTransact(int _hidl_code, HwParcel _hidl_request, final HwParcel _hidl_reply, int _hidl_flags)
|
||||
throws android.os.RemoteException {
|
||||
switch (_hidl_code) {
|
||||
case 1 /* initializationComplete */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
|
||||
int status = _hidl_request.readInt32();
|
||||
initializationComplete(status);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 2 /* hciEventReceived */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> event = _hidl_request.readInt8Vector();
|
||||
hciEventReceived(event);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 3 /* aclDataReceived */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
aclDataReceived(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 4 /* scoDataReceived */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
scoDataReceived(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256067662 /* interfaceChain */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<String> _hidl_out_descriptors = interfaceChain();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.writeStringVector(_hidl_out_descriptors);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256131655 /* debug */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
NativeHandle fd = _hidl_request.readNativeHandle();
|
||||
java.util.ArrayList<String> options = _hidl_request.readStringVector();
|
||||
debug(fd, options);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256136003 /* interfaceDescriptor */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
String _hidl_out_descriptor = interfaceDescriptor();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.writeString(_hidl_out_descriptor);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256398152 /* getHashChain */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<byte[/* 32 */]> _hidl_out_hashchain = getHashChain();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
{
|
||||
HwBlob _hidl_blob = new HwBlob(16 /* size */);
|
||||
{
|
||||
int _hidl_vec_size = _hidl_out_hashchain.size();
|
||||
_hidl_blob.putInt32(0 /* offset */ + 8 /* offsetof(hidl_vec<T>, mSize) */, _hidl_vec_size);
|
||||
_hidl_blob.putBool(0 /* offset */ + 12 /* offsetof(hidl_vec<T>, mOwnsBuffer) */, false);
|
||||
HwBlob childBlob = new HwBlob((int)(_hidl_vec_size * 32));
|
||||
for (int _hidl_index_0 = 0; _hidl_index_0 < _hidl_vec_size; ++_hidl_index_0) {
|
||||
{
|
||||
long _hidl_array_offset_1 = _hidl_index_0 * 32;
|
||||
byte[] _hidl_array_item_1 = (byte[/* 32 */]) _hidl_out_hashchain.get(_hidl_index_0);
|
||||
|
||||
if (_hidl_array_item_1 == null || _hidl_array_item_1.length != 32) {
|
||||
throw new IllegalArgumentException("Array element is not of the expected length");
|
||||
}
|
||||
|
||||
childBlob.putInt8Array(_hidl_array_offset_1, _hidl_array_item_1);
|
||||
_hidl_array_offset_1 += 32 * 1;
|
||||
}
|
||||
}
|
||||
_hidl_blob.putBlob(0 /* offset */ + 0 /* offsetof(hidl_vec<T>, mBuffer) */, childBlob);
|
||||
}
|
||||
_hidl_reply.writeBuffer(_hidl_blob);
|
||||
}
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256462420 /* setHALInstrumentation */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
setHALInstrumentation();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256660548 /* linkToDeath */:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
case 256921159 /* ping */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
ping();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257049926 /* getDebugInfo */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
android.internal.hidl.base.V1_0.DebugInfo _hidl_out_info = getDebugInfo();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
((android.internal.hidl.base.V1_0.DebugInfo) _hidl_out_info).writeToParcel(_hidl_reply);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257120595 /* notifySyspropsChanged */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
notifySyspropsChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257250372 /* unlinkToDeath */:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package android.hardware.bluetooth.V1_0;
|
||||
|
||||
|
||||
public final class Status {
|
||||
public static final int SUCCESS = 0;
|
||||
public static final int TRANSPORT_ERROR = 1 /* ::android::hardware::bluetooth::V1_0::Status.SUCCESS implicitly + 1 */;
|
||||
public static final int INITIALIZATION_ERROR = 2 /* ::android::hardware::bluetooth::V1_0::Status.TRANSPORT_ERROR implicitly + 1 */;
|
||||
public static final int UNKNOWN = 3 /* ::android::hardware::bluetooth::V1_0::Status.INITIALIZATION_ERROR implicitly + 1 */;
|
||||
public static final String toString(int o) {
|
||||
if (o == SUCCESS) {
|
||||
return "SUCCESS";
|
||||
}
|
||||
if (o == TRANSPORT_ERROR) {
|
||||
return "TRANSPORT_ERROR";
|
||||
}
|
||||
if (o == INITIALIZATION_ERROR) {
|
||||
return "INITIALIZATION_ERROR";
|
||||
}
|
||||
if (o == UNKNOWN) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
return "0x" + Integer.toHexString(o);
|
||||
}
|
||||
|
||||
public static final String dumpBitfield(int o) {
|
||||
java.util.ArrayList<String> list = new java.util.ArrayList<>();
|
||||
int flipped = 0;
|
||||
list.add("SUCCESS"); // SUCCESS == 0
|
||||
if ((o & TRANSPORT_ERROR) == TRANSPORT_ERROR) {
|
||||
list.add("TRANSPORT_ERROR");
|
||||
flipped |= TRANSPORT_ERROR;
|
||||
}
|
||||
if ((o & INITIALIZATION_ERROR) == INITIALIZATION_ERROR) {
|
||||
list.add("INITIALIZATION_ERROR");
|
||||
flipped |= INITIALIZATION_ERROR;
|
||||
}
|
||||
if ((o & UNKNOWN) == UNKNOWN) {
|
||||
list.add("UNKNOWN");
|
||||
flipped |= UNKNOWN;
|
||||
}
|
||||
if (o != flipped) {
|
||||
list.add("0x" + Integer.toHexString(o & (~flipped)));
|
||||
}
|
||||
return String.join(" | ", list);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,840 @@
|
||||
package android.hardware.bluetooth.V1_1;
|
||||
|
||||
import android.os.HidlSupport;
|
||||
import android.os.HwBinder;
|
||||
import android.os.IHwBinder;
|
||||
import android.os.HwBlob;
|
||||
import android.os.HwParcel;
|
||||
import android.os.IHwInterface;
|
||||
import android.os.NativeHandle;
|
||||
|
||||
/**
|
||||
* The Host Controller Interface (HCI) is the layer defined by the Bluetooth
|
||||
* specification between the software that runs on the host and the Bluetooth
|
||||
* controller chip. This boundary is the natural choice for a Hardware
|
||||
* Abstraction Layer (HAL). Dealing only in HCI packets and events simplifies
|
||||
* the stack and abstracts away power management, initialization, and other
|
||||
* implementation-specific details related to the hardware.
|
||||
*/
|
||||
public interface IBluetoothHci extends android.hardware.bluetooth.V1_0.IBluetoothHci {
|
||||
/**
|
||||
* Fully-qualified interface name for this interface.
|
||||
*/
|
||||
public static final String kInterfaceName = "android.hardware.bluetooth@1.1::IBluetoothHci";
|
||||
|
||||
/**
|
||||
* Does a checked conversion from a binder to this class.
|
||||
*/
|
||||
/* package private */ static IBluetoothHci asInterface(IHwBinder binder) {
|
||||
if (binder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
IHwInterface iface =
|
||||
binder.queryLocalInterface(kInterfaceName);
|
||||
|
||||
if ((iface != null) && (iface instanceof IBluetoothHci)) {
|
||||
return (IBluetoothHci)iface;
|
||||
}
|
||||
|
||||
IBluetoothHci proxy = new IBluetoothHci.Proxy(binder);
|
||||
|
||||
try {
|
||||
for (String descriptor : proxy.interfaceChain()) {
|
||||
if (descriptor.equals(kInterfaceName)) {
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
} catch (android.os.RemoteException e) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a checked conversion from any interface to this class.
|
||||
*/
|
||||
public static IBluetoothHci castFrom(IHwInterface iface) {
|
||||
return (iface == null) ? null : IBluetoothHci.asInterface(iface.asBinder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwBinder asBinder();
|
||||
|
||||
/**
|
||||
* This will invoke the equivalent of the C++ getService(std::string) if retry is
|
||||
* true or tryGetService(std::string) if retry is false. If the service is
|
||||
* available on the device and retry is true, this will wait for the service to
|
||||
* start.
|
||||
*
|
||||
*/
|
||||
public static IBluetoothHci getService(String serviceName, boolean retry) throws android.os.RemoteException {
|
||||
return IBluetoothHci.asInterface(HwBinder.getService("android.hardware.bluetooth@1.1::IBluetoothHci", serviceName, retry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls getService("default",retry).
|
||||
*/
|
||||
public static IBluetoothHci getService(boolean retry) throws android.os.RemoteException {
|
||||
return getService("default", retry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this will not wait for the interface to come up if it hasn't yet
|
||||
* started. See getService(String,boolean) instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static IBluetoothHci getService(String serviceName) throws android.os.RemoteException {
|
||||
return IBluetoothHci.asInterface(HwBinder.getService("android.hardware.bluetooth@1.1::IBluetoothHci", serviceName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this will not wait for the interface to come up if it hasn't yet
|
||||
* started. See getService(boolean) instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static IBluetoothHci getService() throws android.os.RemoteException {
|
||||
return getService("default");
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as @1.0, but uses 1.1 Callbacks version
|
||||
*/
|
||||
void initialize_1_1(android.hardware.bluetooth.V1_1.IBluetoothHciCallbacks callback)
|
||||
throws android.os.RemoteException;
|
||||
/**
|
||||
* Send an ISO data packet (as specified in the Bluetooth Core
|
||||
* Specification v5.2) to the Bluetooth controller.
|
||||
* Packets must be processed in order.
|
||||
* @param data HCI data packet to be sent
|
||||
*/
|
||||
void sendIsoData(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides run-time type information for this object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceChain on an IChild object must yield the following:
|
||||
* ["android.hardware.foo@1.0::IChild",
|
||||
* "android.hardware.foo@1.0::IParent"
|
||||
* "android.internal.hidl.base@1.0::IBase"]
|
||||
*
|
||||
* @return descriptors a vector of descriptors of the run-time type of the
|
||||
* object.
|
||||
*/
|
||||
java.util.ArrayList<String> interfaceChain()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Emit diagnostic information to the given file.
|
||||
*
|
||||
* Optionally overriden.
|
||||
*
|
||||
* @param fd File descriptor to dump data to.
|
||||
* Must only be used for the duration of this call.
|
||||
* @param options Arguments for debugging.
|
||||
* Must support empty for default debug information.
|
||||
*/
|
||||
void debug(NativeHandle fd, java.util.ArrayList<String> options)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides run-time type information for this object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceDescriptor on an IChild object must yield
|
||||
* "android.hardware.foo@1.0::IChild"
|
||||
*
|
||||
* @return descriptor a descriptor of the run-time type of the
|
||||
* object (the first element of the vector returned by
|
||||
* interfaceChain())
|
||||
*/
|
||||
String interfaceDescriptor()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Returns hashes of the source HAL files that define the interfaces of the
|
||||
* runtime type information on the object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceChain on an IChild object must yield the following:
|
||||
* [(hash of IChild.hal),
|
||||
* (hash of IParent.hal)
|
||||
* (hash of IBase.hal)].
|
||||
*
|
||||
* SHA-256 is used as the hashing algorithm. Each hash has 32 bytes
|
||||
* according to SHA-256 standard.
|
||||
*
|
||||
* @return hashchain a vector of SHA-1 digests
|
||||
*/
|
||||
java.util.ArrayList<byte[/* 32 */]> getHashChain()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* This method trigger the interface to enable/disable instrumentation based
|
||||
* on system property hal.instrumentation.enable.
|
||||
*/
|
||||
void setHALInstrumentation()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Registers a death recipient, to be called when the process hosting this
|
||||
* interface dies.
|
||||
*
|
||||
* @param recipient a hidl_death_recipient callback object
|
||||
* @param cookie a cookie that must be returned with the callback
|
||||
* @return success whether the death recipient was registered successfully.
|
||||
*/
|
||||
boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides way to determine if interface is running without requesting
|
||||
* any functionality.
|
||||
*/
|
||||
void ping()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Get debug information on references on this interface.
|
||||
* @return info debugging information. See comments of DebugInfo.
|
||||
*/
|
||||
android.internal.hidl.base.V1_0.DebugInfo getDebugInfo()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* This method notifies the interface that one or more system properties
|
||||
* have changed. The default implementation calls
|
||||
* (C++) report_sysprop_change() in libcutils or
|
||||
* (Java) android.os.SystemProperties.reportSyspropChanged,
|
||||
* which in turn calls a set of registered callbacks (eg to update trace
|
||||
* tags).
|
||||
*/
|
||||
void notifySyspropsChanged()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Unregisters the registered death recipient. If this service was registered
|
||||
* multiple times with the same exact death recipient, this unlinks the most
|
||||
* recently registered one.
|
||||
*
|
||||
* @param recipient a previously registered hidl_death_recipient callback
|
||||
* @return success whether the death recipient was unregistered successfully.
|
||||
*/
|
||||
boolean unlinkToDeath(IHwBinder.DeathRecipient recipient)
|
||||
throws android.os.RemoteException;
|
||||
|
||||
public static final class Proxy implements IBluetoothHci {
|
||||
private IHwBinder mRemote;
|
||||
|
||||
public Proxy(IHwBinder remote) {
|
||||
mRemote = java.util.Objects.requireNonNull(remote);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwBinder asBinder() {
|
||||
return mRemote;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
try {
|
||||
return this.interfaceDescriptor() + "@Proxy";
|
||||
} catch (android.os.RemoteException ex) {
|
||||
/* ignored; handled below. */
|
||||
}
|
||||
return "[class or subclass of " + IBluetoothHci.kInterfaceName + "]@Proxy";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(java.lang.Object other) {
|
||||
return HidlSupport.interfacesEqual(this, other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int hashCode() {
|
||||
return this.asBinder().hashCode();
|
||||
}
|
||||
|
||||
// Methods from ::android::hardware::bluetooth::V1_0::IBluetoothHci follow.
|
||||
@Override
|
||||
public void initialize(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks callback)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeStrongBinder(callback == null ? null : callback.asBinder());
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(1 /* initialize */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendHciCommand(java.util.ArrayList<Byte> command)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(command);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(2 /* sendHciCommand */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendAclData(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(3 /* sendAclData */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendScoData(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(4 /* sendScoData */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(5 /* close */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Methods from ::android::hardware::bluetooth::V1_1::IBluetoothHci follow.
|
||||
@Override
|
||||
public void initialize_1_1(android.hardware.bluetooth.V1_1.IBluetoothHciCallbacks callback)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_1.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeStrongBinder(callback == null ? null : callback.asBinder());
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(6 /* initialize_1_1 */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendIsoData(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_1.IBluetoothHci.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(7 /* sendIsoData */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Methods from ::android::hidl::base::V1_0::IBase follow.
|
||||
@Override
|
||||
public java.util.ArrayList<String> interfaceChain()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256067662 /* interfaceChain */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
java.util.ArrayList<String> _hidl_out_descriptors = _hidl_reply.readStringVector();
|
||||
return _hidl_out_descriptors;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(NativeHandle fd, java.util.ArrayList<String> options)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
_hidl_request.writeNativeHandle(fd);
|
||||
_hidl_request.writeStringVector(options);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256131655 /* debug */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String interfaceDescriptor()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256136003 /* interfaceDescriptor */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
String _hidl_out_descriptor = _hidl_reply.readString();
|
||||
return _hidl_out_descriptor;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.ArrayList<byte[/* 32 */]> getHashChain()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256398152 /* getHashChain */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
java.util.ArrayList<byte[/* 32 */]> _hidl_out_hashchain = new java.util.ArrayList<byte[/* 32 */]>();
|
||||
{
|
||||
HwBlob _hidl_blob = _hidl_reply.readBuffer(16 /* size */);
|
||||
{
|
||||
int _hidl_vec_size = _hidl_blob.getInt32(0 /* offset */ + 8 /* offsetof(hidl_vec<T>, mSize) */);
|
||||
HwBlob childBlob = _hidl_reply.readEmbeddedBuffer(
|
||||
_hidl_vec_size * 32,_hidl_blob.handle(),
|
||||
0 /* offset */ + 0 /* offsetof(hidl_vec<T>, mBuffer) */,true /* nullable */);
|
||||
|
||||
((java.util.ArrayList<byte[/* 32 */]>) _hidl_out_hashchain).clear();
|
||||
for (int _hidl_index_0 = 0; _hidl_index_0 < _hidl_vec_size; ++_hidl_index_0) {
|
||||
byte[/* 32 */] _hidl_vec_element = new byte[32];
|
||||
{
|
||||
long _hidl_array_offset_1 = _hidl_index_0 * 32;
|
||||
childBlob.copyToInt8Array(_hidl_array_offset_1, (byte[/* 32 */]) _hidl_vec_element, 32 /* size */);
|
||||
_hidl_array_offset_1 += 32 * 1;
|
||||
}
|
||||
((java.util.ArrayList<byte[/* 32 */]>) _hidl_out_hashchain).add(_hidl_vec_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _hidl_out_hashchain;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHALInstrumentation()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256462420 /* setHALInstrumentation */, _hidl_request, _hidl_reply, 1 /* oneway */);
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie)
|
||||
throws android.os.RemoteException {
|
||||
return mRemote.linkToDeath(recipient, cookie);
|
||||
}
|
||||
@Override
|
||||
public void ping()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256921159 /* ping */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.internal.hidl.base.V1_0.DebugInfo getDebugInfo()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(257049926 /* getDebugInfo */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
android.internal.hidl.base.V1_0.DebugInfo _hidl_out_info = new android.internal.hidl.base.V1_0.DebugInfo();
|
||||
((android.internal.hidl.base.V1_0.DebugInfo) _hidl_out_info).readFromParcel(_hidl_reply);
|
||||
return _hidl_out_info;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifySyspropsChanged()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(257120595 /* notifySyspropsChanged */, _hidl_request, _hidl_reply, 1 /* oneway */);
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unlinkToDeath(IHwBinder.DeathRecipient recipient)
|
||||
throws android.os.RemoteException {
|
||||
return mRemote.unlinkToDeath(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Stub extends HwBinder implements IBluetoothHci {
|
||||
@Override
|
||||
public IHwBinder asBinder() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final java.util.ArrayList<String> interfaceChain() {
|
||||
return new java.util.ArrayList<String>(java.util.Arrays.asList(
|
||||
android.hardware.bluetooth.V1_1.IBluetoothHci.kInterfaceName,
|
||||
android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName,
|
||||
android.internal.hidl.base.V1_0.IBase.kInterfaceName));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(NativeHandle fd, java.util.ArrayList<String> options) {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String interfaceDescriptor() {
|
||||
return android.hardware.bluetooth.V1_1.IBluetoothHci.kInterfaceName;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final java.util.ArrayList<byte[/* 32 */]> getHashChain() {
|
||||
return new java.util.ArrayList<byte[/* 32 */]>(java.util.Arrays.asList(
|
||||
new byte[/* 32 */]{54,47,-47,-62,22,65,-62,34,79,59,-128,-61,13,-105,-105,-71,-120,-6,63,52,66,67,-43,49,-70,115,-59,83,119,-102,87,99} /* 362fd1c21641c2224f3b80c30d9797b988fa3f344243d531ba73c553779a5763 */,
|
||||
new byte[/* 32 */]{52,124,-25,70,-127,86,7,86,127,95,59,83,-28,-128,9,-104,-54,90,-71,53,81,65,-16,-120,15,-64,-49,12,31,-59,-61,85} /* 347ce746815607567f5f3b53e4800998ca5ab9355141f0880fc0cf0c1fc5c355 */,
|
||||
new byte[/* 32 */]{-20,127,-41,-98,-48,45,-6,-123,-68,73,-108,38,-83,-82,62,-66,35,-17,5,36,-13,-51,105,87,19,-109,36,-72,59,24,-54,76} /* ec7fd79ed02dfa85bc499426adae3ebe23ef0524f3cd6957139324b83b18ca4c */));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setHALInstrumentation() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie) {
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void ping() {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final android.internal.hidl.base.V1_0.DebugInfo getDebugInfo() {
|
||||
android.internal.hidl.base.V1_0.DebugInfo info = new android.internal.hidl.base.V1_0.DebugInfo();
|
||||
info.pid = HidlSupport.getPidIfSharable();
|
||||
info.ptr = 0;
|
||||
info.arch = android.internal.hidl.base.V1_0.DebugInfo.Architecture.UNKNOWN;
|
||||
return info;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void notifySyspropsChanged() {
|
||||
HwBinder.enableInstrumentation();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean unlinkToDeath(IHwBinder.DeathRecipient recipient) {
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwInterface queryLocalInterface(String descriptor) {
|
||||
if (kInterfaceName.equals(descriptor)) {
|
||||
return this;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void registerAsService(String serviceName) throws android.os.RemoteException {
|
||||
registerService(serviceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.interfaceDescriptor() + "@Stub";
|
||||
}
|
||||
|
||||
//@Override
|
||||
public void onTransact(int _hidl_code, HwParcel _hidl_request, final HwParcel _hidl_reply, int _hidl_flags)
|
||||
throws android.os.RemoteException {
|
||||
switch (_hidl_code) {
|
||||
case 1 /* initialize */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks callback = android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.asInterface(_hidl_request.readStrongBinder());
|
||||
initialize(callback);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 2 /* sendHciCommand */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> command = _hidl_request.readInt8Vector();
|
||||
sendHciCommand(command);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 3 /* sendAclData */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
sendAclData(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 4 /* sendScoData */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
sendScoData(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 5 /* close */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHci.kInterfaceName);
|
||||
|
||||
close();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 6 /* initialize_1_1 */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_1.IBluetoothHci.kInterfaceName);
|
||||
|
||||
android.hardware.bluetooth.V1_1.IBluetoothHciCallbacks callback = android.hardware.bluetooth.V1_1.IBluetoothHciCallbacks.asInterface(_hidl_request.readStrongBinder());
|
||||
initialize_1_1(callback);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 7 /* sendIsoData */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_1.IBluetoothHci.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
sendIsoData(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256067662 /* interfaceChain */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<String> _hidl_out_descriptors = interfaceChain();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.writeStringVector(_hidl_out_descriptors);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256131655 /* debug */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
NativeHandle fd = _hidl_request.readNativeHandle();
|
||||
java.util.ArrayList<String> options = _hidl_request.readStringVector();
|
||||
debug(fd, options);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256136003 /* interfaceDescriptor */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
String _hidl_out_descriptor = interfaceDescriptor();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.writeString(_hidl_out_descriptor);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256398152 /* getHashChain */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<byte[/* 32 */]> _hidl_out_hashchain = getHashChain();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
{
|
||||
HwBlob _hidl_blob = new HwBlob(16 /* size */);
|
||||
{
|
||||
int _hidl_vec_size = _hidl_out_hashchain.size();
|
||||
_hidl_blob.putInt32(0 /* offset */ + 8 /* offsetof(hidl_vec<T>, mSize) */, _hidl_vec_size);
|
||||
_hidl_blob.putBool(0 /* offset */ + 12 /* offsetof(hidl_vec<T>, mOwnsBuffer) */, false);
|
||||
HwBlob childBlob = new HwBlob((int)(_hidl_vec_size * 32));
|
||||
for (int _hidl_index_0 = 0; _hidl_index_0 < _hidl_vec_size; ++_hidl_index_0) {
|
||||
{
|
||||
long _hidl_array_offset_1 = _hidl_index_0 * 32;
|
||||
byte[] _hidl_array_item_1 = (byte[/* 32 */]) _hidl_out_hashchain.get(_hidl_index_0);
|
||||
|
||||
if (_hidl_array_item_1 == null || _hidl_array_item_1.length != 32) {
|
||||
throw new IllegalArgumentException("Array element is not of the expected length");
|
||||
}
|
||||
|
||||
childBlob.putInt8Array(_hidl_array_offset_1, _hidl_array_item_1);
|
||||
_hidl_array_offset_1 += 32 * 1;
|
||||
}
|
||||
}
|
||||
_hidl_blob.putBlob(0 /* offset */ + 0 /* offsetof(hidl_vec<T>, mBuffer) */, childBlob);
|
||||
}
|
||||
_hidl_reply.writeBuffer(_hidl_blob);
|
||||
}
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256462420 /* setHALInstrumentation */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
setHALInstrumentation();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256660548 /* linkToDeath */:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
case 256921159 /* ping */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
ping();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257049926 /* getDebugInfo */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
android.internal.hidl.base.V1_0.DebugInfo _hidl_out_info = getDebugInfo();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
((android.internal.hidl.base.V1_0.DebugInfo) _hidl_out_info).writeToParcel(_hidl_reply);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257120595 /* notifySyspropsChanged */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
notifySyspropsChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257250372 /* unlinkToDeath */:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
package android.hardware.bluetooth.V1_1;
|
||||
|
||||
import android.os.HidlSupport;
|
||||
import android.os.HwBinder;
|
||||
import android.os.IHwBinder;
|
||||
import android.os.HwBlob;
|
||||
import android.os.HwParcel;
|
||||
import android.os.IHwInterface;
|
||||
import android.os.NativeHandle;
|
||||
|
||||
/**
|
||||
* The interface from the Bluetooth Controller to the stack.
|
||||
*/
|
||||
public interface IBluetoothHciCallbacks extends android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks {
|
||||
/**
|
||||
* Fully-qualified interface name for this interface.
|
||||
*/
|
||||
public static final String kInterfaceName = "android.hardware.bluetooth@1.1::IBluetoothHciCallbacks";
|
||||
|
||||
/**
|
||||
* Does a checked conversion from a binder to this class.
|
||||
*/
|
||||
/* package private */ static IBluetoothHciCallbacks asInterface(IHwBinder binder) {
|
||||
if (binder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
IHwInterface iface =
|
||||
binder.queryLocalInterface(kInterfaceName);
|
||||
|
||||
if ((iface != null) && (iface instanceof IBluetoothHciCallbacks)) {
|
||||
return (IBluetoothHciCallbacks)iface;
|
||||
}
|
||||
|
||||
IBluetoothHciCallbacks proxy = new IBluetoothHciCallbacks.Proxy(binder);
|
||||
|
||||
try {
|
||||
for (String descriptor : proxy.interfaceChain()) {
|
||||
if (descriptor.equals(kInterfaceName)) {
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
} catch (android.os.RemoteException e) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a checked conversion from any interface to this class.
|
||||
*/
|
||||
public static IBluetoothHciCallbacks castFrom(IHwInterface iface) {
|
||||
return (iface == null) ? null : IBluetoothHciCallbacks.asInterface(iface.asBinder());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwBinder asBinder();
|
||||
|
||||
/**
|
||||
* This will invoke the equivalent of the C++ getService(std::string) if retry is
|
||||
* true or tryGetService(std::string) if retry is false. If the service is
|
||||
* available on the device and retry is true, this will wait for the service to
|
||||
* start.
|
||||
*
|
||||
*/
|
||||
public static IBluetoothHciCallbacks getService(String serviceName, boolean retry) throws android.os.RemoteException {
|
||||
return IBluetoothHciCallbacks.asInterface(HwBinder.getService("android.hardware.bluetooth@1.1::IBluetoothHciCallbacks", serviceName, retry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls getService("default",retry).
|
||||
*/
|
||||
public static IBluetoothHciCallbacks getService(boolean retry) throws android.os.RemoteException {
|
||||
return getService("default", retry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this will not wait for the interface to come up if it hasn't yet
|
||||
* started. See getService(String,boolean) instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static IBluetoothHciCallbacks getService(String serviceName) throws android.os.RemoteException {
|
||||
return IBluetoothHciCallbacks.asInterface(HwBinder.getService("android.hardware.bluetooth@1.1::IBluetoothHciCallbacks", serviceName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this will not wait for the interface to come up if it hasn't yet
|
||||
* started. See getService(boolean) instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static IBluetoothHciCallbacks getService() throws android.os.RemoteException {
|
||||
return getService("default");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a ISO data packet form the controller to the host.
|
||||
* @param data the ISO HCI packet to be passed to the host stack
|
||||
*/
|
||||
void isoDataReceived(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides run-time type information for this object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceChain on an IChild object must yield the following:
|
||||
* ["android.hardware.foo@1.0::IChild",
|
||||
* "android.hardware.foo@1.0::IParent"
|
||||
* "android.internal.hidl.base@1.0::IBase"]
|
||||
*
|
||||
* @return descriptors a vector of descriptors of the run-time type of the
|
||||
* object.
|
||||
*/
|
||||
java.util.ArrayList<String> interfaceChain()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Emit diagnostic information to the given file.
|
||||
*
|
||||
* Optionally overriden.
|
||||
*
|
||||
* @param fd File descriptor to dump data to.
|
||||
* Must only be used for the duration of this call.
|
||||
* @param options Arguments for debugging.
|
||||
* Must support empty for default debug information.
|
||||
*/
|
||||
void debug(NativeHandle fd, java.util.ArrayList<String> options)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides run-time type information for this object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceDescriptor on an IChild object must yield
|
||||
* "android.hardware.foo@1.0::IChild"
|
||||
*
|
||||
* @return descriptor a descriptor of the run-time type of the
|
||||
* object (the first element of the vector returned by
|
||||
* interfaceChain())
|
||||
*/
|
||||
String interfaceDescriptor()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Returns hashes of the source HAL files that define the interfaces of the
|
||||
* runtime type information on the object.
|
||||
* For example, for the following interface definition:
|
||||
* package android.hardware.foo@1.0;
|
||||
* interface IParent {};
|
||||
* interface IChild extends IParent {};
|
||||
* Calling interfaceChain on an IChild object must yield the following:
|
||||
* [(hash of IChild.hal),
|
||||
* (hash of IParent.hal)
|
||||
* (hash of IBase.hal)].
|
||||
*
|
||||
* SHA-256 is used as the hashing algorithm. Each hash has 32 bytes
|
||||
* according to SHA-256 standard.
|
||||
*
|
||||
* @return hashchain a vector of SHA-1 digests
|
||||
*/
|
||||
java.util.ArrayList<byte[/* 32 */]> getHashChain()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* This method trigger the interface to enable/disable instrumentation based
|
||||
* on system property hal.instrumentation.enable.
|
||||
*/
|
||||
void setHALInstrumentation()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Registers a death recipient, to be called when the process hosting this
|
||||
* interface dies.
|
||||
*
|
||||
* @param recipient a hidl_death_recipient callback object
|
||||
* @param cookie a cookie that must be returned with the callback
|
||||
* @return success whether the death recipient was registered successfully.
|
||||
*/
|
||||
boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie)
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Provides way to determine if interface is running without requesting
|
||||
* any functionality.
|
||||
*/
|
||||
void ping()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Get debug information on references on this interface.
|
||||
* @return info debugging information. See comments of DebugInfo.
|
||||
*/
|
||||
android.internal.hidl.base.V1_0.DebugInfo getDebugInfo()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* This method notifies the interface that one or more system properties
|
||||
* have changed. The default implementation calls
|
||||
* (C++) report_sysprop_change() in libcutils or
|
||||
* (Java) android.os.SystemProperties.reportSyspropChanged,
|
||||
* which in turn calls a set of registered callbacks (eg to update trace
|
||||
* tags).
|
||||
*/
|
||||
void notifySyspropsChanged()
|
||||
throws android.os.RemoteException;
|
||||
/*
|
||||
* Unregisters the registered death recipient. If this service was registered
|
||||
* multiple times with the same exact death recipient, this unlinks the most
|
||||
* recently registered one.
|
||||
*
|
||||
* @param recipient a previously registered hidl_death_recipient callback
|
||||
* @return success whether the death recipient was unregistered successfully.
|
||||
*/
|
||||
boolean unlinkToDeath(IHwBinder.DeathRecipient recipient)
|
||||
throws android.os.RemoteException;
|
||||
|
||||
public static final class Proxy implements IBluetoothHciCallbacks {
|
||||
private IHwBinder mRemote;
|
||||
|
||||
public Proxy(IHwBinder remote) {
|
||||
mRemote = java.util.Objects.requireNonNull(remote);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwBinder asBinder() {
|
||||
return mRemote;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
try {
|
||||
return this.interfaceDescriptor() + "@Proxy";
|
||||
} catch (android.os.RemoteException ex) {
|
||||
/* ignored; handled below. */
|
||||
}
|
||||
return "[class or subclass of " + IBluetoothHciCallbacks.kInterfaceName + "]@Proxy";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean equals(java.lang.Object other) {
|
||||
return HidlSupport.interfacesEqual(this, other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int hashCode() {
|
||||
return this.asBinder().hashCode();
|
||||
}
|
||||
|
||||
// Methods from ::android::hardware::bluetooth::V1_0::IBluetoothHciCallbacks follow.
|
||||
@Override
|
||||
public void initializationComplete(int status)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
_hidl_request.writeInt32(status);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(1 /* initializationComplete */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hciEventReceived(java.util.ArrayList<Byte> event)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(event);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(2 /* hciEventReceived */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void aclDataReceived(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(3 /* aclDataReceived */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scoDataReceived(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(4 /* scoDataReceived */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Methods from ::android::hardware::bluetooth::V1_1::IBluetoothHciCallbacks follow.
|
||||
@Override
|
||||
public void isoDataReceived(java.util.ArrayList<Byte> data)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.hardware.bluetooth.V1_1.IBluetoothHciCallbacks.kInterfaceName);
|
||||
_hidl_request.writeInt8Vector(data);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(5 /* isoDataReceived */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Methods from ::android::hidl::base::V1_0::IBase follow.
|
||||
@Override
|
||||
public java.util.ArrayList<String> interfaceChain()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256067662 /* interfaceChain */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
java.util.ArrayList<String> _hidl_out_descriptors = _hidl_reply.readStringVector();
|
||||
return _hidl_out_descriptors;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(NativeHandle fd, java.util.ArrayList<String> options)
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
_hidl_request.writeNativeHandle(fd);
|
||||
_hidl_request.writeStringVector(options);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256131655 /* debug */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String interfaceDescriptor()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256136003 /* interfaceDescriptor */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
String _hidl_out_descriptor = _hidl_reply.readString();
|
||||
return _hidl_out_descriptor;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.ArrayList<byte[/* 32 */]> getHashChain()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256398152 /* getHashChain */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
java.util.ArrayList<byte[/* 32 */]> _hidl_out_hashchain = new java.util.ArrayList<byte[/* 32 */]>();
|
||||
{
|
||||
HwBlob _hidl_blob = _hidl_reply.readBuffer(16 /* size */);
|
||||
{
|
||||
int _hidl_vec_size = _hidl_blob.getInt32(0 /* offset */ + 8 /* offsetof(hidl_vec<T>, mSize) */);
|
||||
HwBlob childBlob = _hidl_reply.readEmbeddedBuffer(
|
||||
_hidl_vec_size * 32,_hidl_blob.handle(),
|
||||
0 /* offset */ + 0 /* offsetof(hidl_vec<T>, mBuffer) */,true /* nullable */);
|
||||
|
||||
((java.util.ArrayList<byte[/* 32 */]>) _hidl_out_hashchain).clear();
|
||||
for (int _hidl_index_0 = 0; _hidl_index_0 < _hidl_vec_size; ++_hidl_index_0) {
|
||||
byte[/* 32 */] _hidl_vec_element = new byte[32];
|
||||
{
|
||||
long _hidl_array_offset_1 = _hidl_index_0 * 32;
|
||||
childBlob.copyToInt8Array(_hidl_array_offset_1, (byte[/* 32 */]) _hidl_vec_element, 32 /* size */);
|
||||
_hidl_array_offset_1 += 32 * 1;
|
||||
}
|
||||
((java.util.ArrayList<byte[/* 32 */]>) _hidl_out_hashchain).add(_hidl_vec_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _hidl_out_hashchain;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHALInstrumentation()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256462420 /* setHALInstrumentation */, _hidl_request, _hidl_reply, 1 /* oneway */);
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie)
|
||||
throws android.os.RemoteException {
|
||||
return mRemote.linkToDeath(recipient, cookie);
|
||||
}
|
||||
@Override
|
||||
public void ping()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(256921159 /* ping */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.internal.hidl.base.V1_0.DebugInfo getDebugInfo()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(257049926 /* getDebugInfo */, _hidl_request, _hidl_reply, 0 /* flags */);
|
||||
_hidl_reply.verifySuccess();
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
|
||||
android.internal.hidl.base.V1_0.DebugInfo _hidl_out_info = new android.internal.hidl.base.V1_0.DebugInfo();
|
||||
((android.internal.hidl.base.V1_0.DebugInfo) _hidl_out_info).readFromParcel(_hidl_reply);
|
||||
return _hidl_out_info;
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifySyspropsChanged()
|
||||
throws android.os.RemoteException {
|
||||
HwParcel _hidl_request = new HwParcel();
|
||||
_hidl_request.writeInterfaceToken(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
HwParcel _hidl_reply = new HwParcel();
|
||||
try {
|
||||
mRemote.transact(257120595 /* notifySyspropsChanged */, _hidl_request, _hidl_reply, 1 /* oneway */);
|
||||
_hidl_request.releaseTemporaryStorage();
|
||||
} finally {
|
||||
_hidl_reply.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unlinkToDeath(IHwBinder.DeathRecipient recipient)
|
||||
throws android.os.RemoteException {
|
||||
return mRemote.unlinkToDeath(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Stub extends HwBinder implements IBluetoothHciCallbacks {
|
||||
@Override
|
||||
public IHwBinder asBinder() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final java.util.ArrayList<String> interfaceChain() {
|
||||
return new java.util.ArrayList<String>(java.util.Arrays.asList(
|
||||
android.hardware.bluetooth.V1_1.IBluetoothHciCallbacks.kInterfaceName,
|
||||
android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName,
|
||||
android.internal.hidl.base.V1_0.IBase.kInterfaceName));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(NativeHandle fd, java.util.ArrayList<String> options) {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String interfaceDescriptor() {
|
||||
return android.hardware.bluetooth.V1_1.IBluetoothHciCallbacks.kInterfaceName;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final java.util.ArrayList<byte[/* 32 */]> getHashChain() {
|
||||
return new java.util.ArrayList<byte[/* 32 */]>(java.util.Arrays.asList(
|
||||
new byte[/* 32 */]{64,-85,44,104,102,-63,-115,50,-70,-10,-28,-98,48,83,-108,-98,121,96,31,86,-106,58,121,30,-109,-26,-117,-98,-31,-113,113,-115} /* 40ab2c6866c18d32baf6e49e3053949e79601f56963a791e93e68b9ee18f718d */,
|
||||
new byte[/* 32 */]{-125,95,65,-66,34,-127,-65,-78,47,62,51,-58,-6,-121,11,-34,123,-62,30,55,-27,-49,-70,-7,-93,111,-1,23,6,50,-9,84} /* 835f41be2281bfb22f3e33c6fa870bde7bc21e37e5cfbaf9a36fff170632f754 */,
|
||||
new byte[/* 32 */]{-20,127,-41,-98,-48,45,-6,-123,-68,73,-108,38,-83,-82,62,-66,35,-17,5,36,-13,-51,105,87,19,-109,36,-72,59,24,-54,76} /* ec7fd79ed02dfa85bc499426adae3ebe23ef0524f3cd6957139324b83b18ca4c */));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setHALInstrumentation() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie) {
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void ping() {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final android.internal.hidl.base.V1_0.DebugInfo getDebugInfo() {
|
||||
android.internal.hidl.base.V1_0.DebugInfo info = new android.internal.hidl.base.V1_0.DebugInfo();
|
||||
info.pid = HidlSupport.getPidIfSharable();
|
||||
info.ptr = 0;
|
||||
info.arch = android.internal.hidl.base.V1_0.DebugInfo.Architecture.UNKNOWN;
|
||||
return info;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void notifySyspropsChanged() {
|
||||
HwBinder.enableInstrumentation();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean unlinkToDeath(IHwBinder.DeathRecipient recipient) {
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHwInterface queryLocalInterface(String descriptor) {
|
||||
if (kInterfaceName.equals(descriptor)) {
|
||||
return this;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void registerAsService(String serviceName) throws android.os.RemoteException {
|
||||
registerService(serviceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.interfaceDescriptor() + "@Stub";
|
||||
}
|
||||
|
||||
//@Override
|
||||
public void onTransact(int _hidl_code, HwParcel _hidl_request, final HwParcel _hidl_reply, int _hidl_flags)
|
||||
throws android.os.RemoteException {
|
||||
switch (_hidl_code) {
|
||||
case 1 /* initializationComplete */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
|
||||
int status = _hidl_request.readInt32();
|
||||
initializationComplete(status);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 2 /* hciEventReceived */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> event = _hidl_request.readInt8Vector();
|
||||
hciEventReceived(event);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 3 /* aclDataReceived */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
aclDataReceived(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 4 /* scoDataReceived */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
scoDataReceived(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 5 /* isoDataReceived */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.hardware.bluetooth.V1_1.IBluetoothHciCallbacks.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<Byte> data = _hidl_request.readInt8Vector();
|
||||
isoDataReceived(data);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256067662 /* interfaceChain */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<String> _hidl_out_descriptors = interfaceChain();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.writeStringVector(_hidl_out_descriptors);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256131655 /* debug */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
NativeHandle fd = _hidl_request.readNativeHandle();
|
||||
java.util.ArrayList<String> options = _hidl_request.readStringVector();
|
||||
debug(fd, options);
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256136003 /* interfaceDescriptor */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
String _hidl_out_descriptor = interfaceDescriptor();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.writeString(_hidl_out_descriptor);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256398152 /* getHashChain */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
java.util.ArrayList<byte[/* 32 */]> _hidl_out_hashchain = getHashChain();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
{
|
||||
HwBlob _hidl_blob = new HwBlob(16 /* size */);
|
||||
{
|
||||
int _hidl_vec_size = _hidl_out_hashchain.size();
|
||||
_hidl_blob.putInt32(0 /* offset */ + 8 /* offsetof(hidl_vec<T>, mSize) */, _hidl_vec_size);
|
||||
_hidl_blob.putBool(0 /* offset */ + 12 /* offsetof(hidl_vec<T>, mOwnsBuffer) */, false);
|
||||
HwBlob childBlob = new HwBlob((int)(_hidl_vec_size * 32));
|
||||
for (int _hidl_index_0 = 0; _hidl_index_0 < _hidl_vec_size; ++_hidl_index_0) {
|
||||
{
|
||||
long _hidl_array_offset_1 = _hidl_index_0 * 32;
|
||||
byte[] _hidl_array_item_1 = (byte[/* 32 */]) _hidl_out_hashchain.get(_hidl_index_0);
|
||||
|
||||
if (_hidl_array_item_1 == null || _hidl_array_item_1.length != 32) {
|
||||
throw new IllegalArgumentException("Array element is not of the expected length");
|
||||
}
|
||||
|
||||
childBlob.putInt8Array(_hidl_array_offset_1, _hidl_array_item_1);
|
||||
_hidl_array_offset_1 += 32 * 1;
|
||||
}
|
||||
}
|
||||
_hidl_blob.putBlob(0 /* offset */ + 0 /* offsetof(hidl_vec<T>, mBuffer) */, childBlob);
|
||||
}
|
||||
_hidl_reply.writeBuffer(_hidl_blob);
|
||||
}
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256462420 /* setHALInstrumentation */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
setHALInstrumentation();
|
||||
break;
|
||||
}
|
||||
|
||||
case 256660548 /* linkToDeath */:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
case 256921159 /* ping */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
ping();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257049926 /* getDebugInfo */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
android.internal.hidl.base.V1_0.DebugInfo _hidl_out_info = getDebugInfo();
|
||||
_hidl_reply.writeStatus(HwParcel.STATUS_SUCCESS);
|
||||
((android.internal.hidl.base.V1_0.DebugInfo) _hidl_out_info).writeToParcel(_hidl_reply);
|
||||
_hidl_reply.send();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257120595 /* notifySyspropsChanged */:
|
||||
{
|
||||
_hidl_request.enforceInterface(android.internal.hidl.base.V1_0.IBase.kInterfaceName);
|
||||
|
||||
notifySyspropsChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
case 257250372 /* unlinkToDeath */:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package com.github.google.bumble.remotehci;
|
||||
|
||||
import android.hardware.bluetooth.V1_0.Status;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
public interface HciHal {
|
||||
public enum Status {
|
||||
SUCCESS("SUCCESS"),
|
||||
ALREADY_INITIALIZED("ALREADY_INITIALIZED"),
|
||||
UNABLE_TO_OPEN_INTERFACE("UNABLE_TO_OPEN_INTERFACE"),
|
||||
INITIALIZATION_ERROR("INITIALIZATION_ERROR"),
|
||||
TRANSPORT_ERROR("TRANSPORT_ERROR"),
|
||||
UNKNOWN("UNKNOWN");
|
||||
|
||||
public final String label;
|
||||
|
||||
private Status(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
static final String TAG = "HciHal";
|
||||
public static HciHal create(HciHalCallback hciCallbacks) {
|
||||
// First try HIDL
|
||||
HciHal hciHal = HciHidlHal.create(hciCallbacks);
|
||||
if (hciHal != null) {
|
||||
Log.d(TAG, "Found HIDL HAL");
|
||||
return hciHal;
|
||||
}
|
||||
|
||||
// Then try AIDL
|
||||
hciHal = HciAidlHal.create(hciCallbacks);
|
||||
if (hciHal != null) {
|
||||
Log.d(TAG, "Found AIDL HAL");
|
||||
return hciHal;
|
||||
}
|
||||
|
||||
Log.d(TAG, "No HAL found");
|
||||
return null;
|
||||
}
|
||||
|
||||
public Status initialize() throws RemoteException, InterruptedException;
|
||||
public void sendPacket(HciPacket.Type type, byte[] packet);
|
||||
}
|
||||
|
||||
class HciHidlHal extends android.hardware.bluetooth.V1_0.IBluetoothHciCallbacks.Stub implements HciHal {
|
||||
private static final String TAG = "HciHidlHal";
|
||||
private final android.hardware.bluetooth.V1_0.IBluetoothHci mHciService;
|
||||
private final HciHalCallback mHciCallbacks;
|
||||
private int mInitializationStatus = -1;
|
||||
|
||||
|
||||
public static HciHidlHal create(HciHalCallback hciCallbacks) {
|
||||
// Get the HAL service.
|
||||
android.hardware.bluetooth.V1_0.IBluetoothHci hciService;
|
||||
try {
|
||||
hciService = android.hardware.bluetooth.V1_0.IBluetoothHci.getService(true);
|
||||
} catch (NoSuchElementException e) {
|
||||
Log.d(TAG, "HIDL HAL V1.0 not found");
|
||||
return null;
|
||||
} catch (android.os.RemoteException e) {
|
||||
Log.w(TAG, "Exception from getService: " + e);
|
||||
return null;
|
||||
}
|
||||
Log.d(TAG, "Found HIDL HAL V1.0");
|
||||
return new HciHidlHal(hciService, hciCallbacks);
|
||||
}
|
||||
|
||||
private HciHidlHal(android.hardware.bluetooth.V1_0.IBluetoothHci hciService, HciHalCallback hciCallbacks) {
|
||||
mHciService = hciService;
|
||||
mHciCallbacks = hciCallbacks;
|
||||
}
|
||||
|
||||
public Status initialize() throws RemoteException, InterruptedException {
|
||||
// Trigger the initialization.
|
||||
mHciService.initialize(this);
|
||||
|
||||
// Wait for the initialization to complete.
|
||||
Log.d(TAG, "Waiting for initialization status...");
|
||||
synchronized (this) {
|
||||
while (mInitializationStatus == -1) {
|
||||
wait();
|
||||
}
|
||||
}
|
||||
|
||||
// Map the status code.
|
||||
switch (mInitializationStatus) {
|
||||
case android.hardware.bluetooth.V1_0.Status.SUCCESS:
|
||||
return Status.SUCCESS;
|
||||
|
||||
case android.hardware.bluetooth.V1_0.Status.TRANSPORT_ERROR:
|
||||
return Status.TRANSPORT_ERROR;
|
||||
|
||||
case android.hardware.bluetooth.V1_0.Status.INITIALIZATION_ERROR:
|
||||
return Status.INITIALIZATION_ERROR;
|
||||
|
||||
default:
|
||||
return Status.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacket(HciPacket.Type type, byte[] packet) {
|
||||
ArrayList<Byte> data = HciPacket.byteArrayToList(packet);
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case COMMAND:
|
||||
mHciService.sendHciCommand(data);
|
||||
break;
|
||||
|
||||
case ACL_DATA:
|
||||
mHciService.sendAclData(data);
|
||||
break;
|
||||
|
||||
case SCO_DATA:
|
||||
mHciService.sendScoData(data);
|
||||
break;
|
||||
}
|
||||
} catch (RemoteException error) {
|
||||
Log.w(TAG, "failed to forward packet: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void initializationComplete(int status) throws RemoteException {
|
||||
mInitializationStatus = status;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hciEventReceived(ArrayList<Byte> event) throws RemoteException {
|
||||
byte[] packet = HciPacket.listToByteArray(event);
|
||||
mHciCallbacks.onPacket(HciPacket.Type.EVENT, packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void aclDataReceived(ArrayList<Byte> data) throws RemoteException {
|
||||
byte[] packet = HciPacket.listToByteArray(data);
|
||||
mHciCallbacks.onPacket(HciPacket.Type.ACL_DATA, packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scoDataReceived(ArrayList<Byte> data) throws RemoteException {
|
||||
byte[] packet = HciPacket.listToByteArray(data);
|
||||
mHciCallbacks.onPacket(HciPacket.Type.SCO_DATA, packet);
|
||||
}
|
||||
}
|
||||
|
||||
class HciAidlHal extends android.hardware.bluetooth.IBluetoothHciCallbacks.Stub implements HciHal {
|
||||
private static final String TAG = "HciAidlHal";
|
||||
private final android.hardware.bluetooth.IBluetoothHci mHciService;
|
||||
private final HciHalCallback mHciCallbacks;
|
||||
private int mInitializationStatus = android.hardware.bluetooth.Status.SUCCESS;
|
||||
|
||||
public static HciAidlHal create(HciHalCallback hciCallbacks) {
|
||||
IBinder binder = ServiceManager.getService("android.hardware.bluetooth.IBluetoothHci/default");
|
||||
if (binder == null) {
|
||||
Log.d(TAG, "AIDL HAL not found");
|
||||
return null;
|
||||
}
|
||||
android.hardware.bluetooth.IBluetoothHci hciService = android.hardware.bluetooth.IBluetoothHci.Stub.asInterface(binder);
|
||||
return new HciAidlHal(hciService, hciCallbacks);
|
||||
}
|
||||
|
||||
private HciAidlHal(android.hardware.bluetooth.IBluetoothHci hciService, HciHalCallback hciCallbacks) {
|
||||
super();
|
||||
mHciService = hciService;
|
||||
mHciCallbacks = hciCallbacks;
|
||||
}
|
||||
|
||||
public Status initialize() throws RemoteException, InterruptedException {
|
||||
// Trigger the initialization.
|
||||
mHciService.initialize(this);
|
||||
|
||||
// Wait for the initialization to complete.
|
||||
Log.d(TAG, "Waiting for initialization status...");
|
||||
synchronized (this) {
|
||||
while (mInitializationStatus == -1) {
|
||||
wait();
|
||||
}
|
||||
}
|
||||
|
||||
// Map the status code.
|
||||
switch (mInitializationStatus) {
|
||||
case android.hardware.bluetooth.Status.SUCCESS:
|
||||
return Status.SUCCESS;
|
||||
|
||||
case android.hardware.bluetooth.Status.ALREADY_INITIALIZED:
|
||||
return Status.ALREADY_INITIALIZED;
|
||||
|
||||
case android.hardware.bluetooth.Status.UNABLE_TO_OPEN_INTERFACE:
|
||||
return Status.UNABLE_TO_OPEN_INTERFACE;
|
||||
|
||||
case android.hardware.bluetooth.Status.HARDWARE_INITIALIZATION_ERROR:
|
||||
return Status.INITIALIZATION_ERROR;
|
||||
|
||||
default:
|
||||
return Status.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// HciHal methods.
|
||||
@Override
|
||||
public void sendPacket(HciPacket.Type type, byte[] packet) {
|
||||
try {
|
||||
switch (type) {
|
||||
case COMMAND:
|
||||
mHciService.sendHciCommand(packet);
|
||||
break;
|
||||
|
||||
case ACL_DATA:
|
||||
mHciService.sendAclData(packet);
|
||||
break;
|
||||
|
||||
case SCO_DATA:
|
||||
mHciService.sendScoData(packet);
|
||||
break;
|
||||
|
||||
case ISO_DATA:
|
||||
mHciService.sendIsoData(packet);
|
||||
break;
|
||||
}
|
||||
} catch (RemoteException error) {
|
||||
Log.w(TAG, "failed to forward packet: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
// IBluetoothHciCallbacks methods.
|
||||
@Override
|
||||
public synchronized void initializationComplete(int status) throws RemoteException {
|
||||
mInitializationStatus = status;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hciEventReceived(byte[] event) throws RemoteException {
|
||||
mHciCallbacks.onPacket(HciPacket.Type.EVENT, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void aclDataReceived(byte[] data) throws RemoteException {
|
||||
mHciCallbacks.onPacket(HciPacket.Type.ACL_DATA, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scoDataReceived(byte[] data) throws RemoteException {
|
||||
mHciCallbacks.onPacket(HciPacket.Type.SCO_DATA, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void isoDataReceived(byte[] data) throws RemoteException {
|
||||
mHciCallbacks.onPacket(HciPacket.Type.ISO_DATA, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.github.google.bumble.remotehci;
|
||||
|
||||
public interface HciHalCallback {
|
||||
public void onPacket(HciPacket.Type type, byte[] packet);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.github.google.bumble.remotehci;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class HciPacket {
|
||||
public enum Type {
|
||||
COMMAND((byte) 1),
|
||||
ACL_DATA((byte) 2),
|
||||
SCO_DATA((byte) 3),
|
||||
EVENT((byte) 4),
|
||||
ISO_DATA((byte)5);
|
||||
|
||||
final byte value;
|
||||
final int lengthSize;
|
||||
final int lengthOffset;
|
||||
|
||||
Type(byte value) throws IllegalArgumentException {
|
||||
switch (value) {
|
||||
case 1:
|
||||
case 3:
|
||||
lengthSize = 1;
|
||||
lengthOffset = 2;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
case 5:
|
||||
lengthSize = 2;
|
||||
lengthOffset = 2;
|
||||
break;
|
||||
|
||||
case 4:
|
||||
lengthSize = 1;
|
||||
lengthOffset = 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
}
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static Type fromValue(byte value) {
|
||||
for (Type type : values()) {
|
||||
if (type.value == value) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ArrayList<Byte> byteArrayToList(byte[] byteArray) {
|
||||
ArrayList<Byte> list = new ArrayList<>();
|
||||
list.ensureCapacity(byteArray.length);
|
||||
for (byte x : byteArray) {
|
||||
list.add(x);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static byte[] listToByteArray(ArrayList<Byte> byteList) {
|
||||
byte[] byteArray = new byte[byteList.size()];
|
||||
for (int i = 0; i < byteList.size(); i++) {
|
||||
byteArray[i] = byteList.get(i);
|
||||
}
|
||||
return byteArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.github.google.bumble.remotehci;
|
||||
|
||||
import static java.lang.Integer.min;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
|
||||
class HciParser {
|
||||
Sink sink;
|
||||
State state;
|
||||
int bytesNeeded;
|
||||
ByteArrayOutputStream packet = new ByteArrayOutputStream();
|
||||
HciPacket.Type packetType;
|
||||
|
||||
HciParser(Sink sink) {
|
||||
this.sink = sink;
|
||||
reset();
|
||||
}
|
||||
|
||||
void feedData(byte[] data, int dataSize) {
|
||||
int dataOffset = 0;
|
||||
int dataLeft = dataSize;
|
||||
|
||||
while (dataLeft > 0 && bytesNeeded > 0) {
|
||||
int consumed = min(dataLeft, bytesNeeded);
|
||||
if (state != State.NEED_TYPE) {
|
||||
packet.write(data, dataOffset, consumed);
|
||||
}
|
||||
bytesNeeded -= consumed;
|
||||
|
||||
if (bytesNeeded == 0) {
|
||||
if (state == State.NEED_TYPE) {
|
||||
packetType = HciPacket.Type.fromValue(data[dataOffset]);
|
||||
if (packetType == null) {
|
||||
throw new InvalidFormatException();
|
||||
}
|
||||
bytesNeeded = packetType.lengthOffset + packetType.lengthSize;
|
||||
state = State.NEED_LENGTH;
|
||||
} else if (state == State.NEED_LENGTH) {
|
||||
ByteBuffer lengthBuffer =
|
||||
ByteBuffer.wrap(packet.toByteArray())
|
||||
.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bytesNeeded = packetType.lengthSize == 1 ?
|
||||
lengthBuffer.get(packetType.lengthOffset) & 0xFF :
|
||||
lengthBuffer.getShort(packetType.lengthOffset) & 0xFFFF;
|
||||
state = State.NEED_BODY;
|
||||
}
|
||||
|
||||
// Emit a packet if one is complete.
|
||||
if (state == State.NEED_BODY && bytesNeeded == 0) {
|
||||
if (sink != null) {
|
||||
sink.onPacket(packetType, packet.toByteArray());
|
||||
}
|
||||
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
dataOffset += consumed;
|
||||
dataLeft -= consumed;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
state = State.NEED_TYPE;
|
||||
bytesNeeded = 1;
|
||||
packet.reset();
|
||||
packetType = null;
|
||||
}
|
||||
|
||||
enum State {
|
||||
NEED_TYPE, NEED_LENGTH, NEED_BODY
|
||||
}
|
||||
|
||||
interface Sink {
|
||||
void onPacket(HciPacket.Type type, byte[] packet);
|
||||
}
|
||||
|
||||
static class InvalidFormatException extends RuntimeException {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.github.google.bumble.remotehci;
|
||||
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HciProxy {
|
||||
private static final String TAG = "HciProxy";
|
||||
private final HciServer mServer;
|
||||
private final Listener mListener;
|
||||
private int mCommandPacketsReceived;
|
||||
private int mAclPacketsReceived;
|
||||
private int mScoPacketsReceived;
|
||||
private int mEventPacketsSent;
|
||||
private int mAclPacketsSent;
|
||||
private int mScoPacketsSent;
|
||||
|
||||
HciProxy(int port, Listener listener) throws HalException {
|
||||
this.mListener = listener;
|
||||
|
||||
// Instantiate a HAL to communicate with the hardware.
|
||||
HciHal hciHal = HciHal.create(new HciHalCallback() {
|
||||
@Override
|
||||
public void onPacket(HciPacket.Type type, byte[] packet) {
|
||||
mServer.sendPacket(type, packet);
|
||||
|
||||
switch (type) {
|
||||
case EVENT:
|
||||
mEventPacketsSent += 1;
|
||||
break;
|
||||
|
||||
case ACL_DATA:
|
||||
mAclPacketsSent += 1;
|
||||
break;
|
||||
|
||||
case SCO_DATA:
|
||||
mScoPacketsSent += 1;
|
||||
break;
|
||||
}
|
||||
updateHciPacketCount();
|
||||
}
|
||||
});
|
||||
if (hciHal == null) {
|
||||
String message = "Could not instantiate a HAL instance";
|
||||
Log.w(TAG, message);
|
||||
throw new HalException(message);
|
||||
}
|
||||
|
||||
// Initialize the HAL.
|
||||
HciHal.Status status = null;
|
||||
try {
|
||||
status = hciHal.initialize();
|
||||
} catch (RemoteException | InterruptedException e) {
|
||||
throw new HalException("Exception while initializing");
|
||||
}
|
||||
if (status != HciHal.Status.SUCCESS) {
|
||||
String message = "HAL initialization failed: " + status.label;
|
||||
Log.w(TAG, message);
|
||||
throw new HalException(message);
|
||||
}
|
||||
|
||||
// Create a server to accept clients.
|
||||
mServer = new HciServer(port, new HciServer.Listener() {
|
||||
@Override
|
||||
public void onHostConnectionState(boolean connected) {
|
||||
mListener.onHostConnectionState(connected);
|
||||
if (connected) {
|
||||
mCommandPacketsReceived = 0;
|
||||
mAclPacketsReceived = 0;
|
||||
mScoPacketsReceived = 0;
|
||||
mEventPacketsSent = 0;
|
||||
mAclPacketsSent = 0;
|
||||
mScoPacketsSent = 0;
|
||||
updateHciPacketCount();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(String message) {
|
||||
listener.onMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacket(HciPacket.Type type, byte[] packet) {
|
||||
Log.d(TAG, String.format("onPacket: type=%s, size=%d", type, packet.length));
|
||||
hciHal.sendPacket(type, packet);
|
||||
|
||||
switch (type) {
|
||||
case COMMAND:
|
||||
mCommandPacketsReceived += 1;
|
||||
break;
|
||||
|
||||
case ACL_DATA:
|
||||
mAclPacketsReceived += 1;
|
||||
break;
|
||||
|
||||
case SCO_DATA:
|
||||
mScoPacketsReceived += 1;
|
||||
break;
|
||||
}
|
||||
updateHciPacketCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void run() throws IOException {
|
||||
mServer.run();
|
||||
}
|
||||
|
||||
private void updateHciPacketCount() {
|
||||
mListener.onHciPacketCountChange(
|
||||
mCommandPacketsReceived,
|
||||
mAclPacketsReceived,
|
||||
mScoPacketsReceived,
|
||||
mEventPacketsSent,
|
||||
mAclPacketsSent,
|
||||
mScoPacketsSent
|
||||
);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onHostConnectionState(boolean connected);
|
||||
|
||||
void onHciPacketCountChange(
|
||||
int commandPacketsReceived,
|
||||
int aclPacketsReceived,
|
||||
int scoPacketsReceived,
|
||||
int eventPacketsSent,
|
||||
int aclPacketsSent,
|
||||
int scoPacketsSent
|
||||
);
|
||||
|
||||
void onMessage(String message);
|
||||
}
|
||||
|
||||
public static class HalException extends RuntimeException {
|
||||
public final String message;
|
||||
public HalException(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.github.google.bumble.remotehci;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
|
||||
public class HciServer {
|
||||
private static final String TAG = "HciServer";
|
||||
private static final int BUFFER_SIZE = 1024;
|
||||
private final int mPort;
|
||||
private final Listener mListener;
|
||||
private OutputStream mOutputStream;
|
||||
|
||||
public interface Listener extends HciParser.Sink {
|
||||
void onHostConnectionState(boolean connected);
|
||||
void onMessage(String message);
|
||||
}
|
||||
|
||||
HciServer(int port, Listener listener) {
|
||||
this.mPort = port;
|
||||
this.mListener = listener;
|
||||
}
|
||||
|
||||
public void run() throws IOException {
|
||||
for (;;) {
|
||||
try {
|
||||
loop();
|
||||
} catch (IOException error) {
|
||||
mListener.onMessage("Cannot listen on port " + mPort);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loop() throws IOException {
|
||||
mListener.onHostConnectionState(false);
|
||||
try (ServerSocket serverSocket = new ServerSocket(mPort)) {
|
||||
mListener.onMessage("Waiting for connection on port " + serverSocket.getLocalPort());
|
||||
try (Socket clientSocket = serverSocket.accept()) {
|
||||
mListener.onHostConnectionState(true);
|
||||
mListener.onMessage("Connected");
|
||||
HciParser parser = new HciParser(mListener);
|
||||
InputStream inputStream = clientSocket.getInputStream();
|
||||
synchronized (this) {
|
||||
mOutputStream = clientSocket.getOutputStream();
|
||||
}
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
|
||||
try {
|
||||
for (; ; ) {
|
||||
int bytesRead = inputStream.read(buffer);
|
||||
if (bytesRead < 0) {
|
||||
Log.d(TAG, "end of stream");
|
||||
break;
|
||||
}
|
||||
parser.feedData(buffer, bytesRead);
|
||||
}
|
||||
} catch (IOException error) {
|
||||
Log.d(TAG, "exception in read loop: " + error);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
synchronized (this) {
|
||||
mOutputStream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendPacket(HciPacket.Type type, byte[] packet) {
|
||||
// Create a combined data buffer so we can write it out in a single call.
|
||||
byte[] data = new byte[packet.length + 1];
|
||||
data[0] = type.value;
|
||||
System.arraycopy(packet, 0, data, 1, packet.length);
|
||||
|
||||
synchronized (this) {
|
||||
if (mOutputStream != null) {
|
||||
try {
|
||||
mOutputStream.write(data);
|
||||
} catch (IOException error) {
|
||||
Log.w(TAG, "failed to write packet: " + error);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "no client, dropping packet");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package com.github.google.bumble.remotehci
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.google.bumble.remotehci.HciProxy.HalException
|
||||
import com.github.google.bumble.remotehci.ui.theme.RemoteHCITheme
|
||||
import java.io.IOException
|
||||
import java.util.logging.Logger
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
const val DEFAULT_TCP_PORT = 9993
|
||||
const val TCP_PORT_PREF_KEY = "tcp_port"
|
||||
|
||||
class AppViewModel : ViewModel(), HciProxy.Listener {
|
||||
private var preferences: SharedPreferences? = null
|
||||
var tcpPort by mutableStateOf(DEFAULT_TCP_PORT)
|
||||
var canStart by mutableStateOf(true)
|
||||
var message by mutableStateOf("")
|
||||
var hostConnected by mutableStateOf(false)
|
||||
var hciCommandPacketsReceived by mutableStateOf(0)
|
||||
var hciAclPacketsReceived by mutableStateOf(0)
|
||||
var hciScoPacketsReceived by mutableStateOf(0)
|
||||
var hciEventPacketsSent by mutableStateOf(0)
|
||||
var hciAclPacketsSent by mutableStateOf(0)
|
||||
var hciScoPacketsSent by mutableStateOf(0)
|
||||
|
||||
fun loadPreferences(preferences: SharedPreferences) {
|
||||
this.preferences = preferences
|
||||
val savedTcpPortString = preferences.getString(TCP_PORT_PREF_KEY, null)
|
||||
if (savedTcpPortString != null) {
|
||||
val savedTcpPortInt = savedTcpPortString.toIntOrNull()
|
||||
if (savedTcpPortInt != null) {
|
||||
tcpPort = savedTcpPortInt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTcpPort(tcpPort: Int) {
|
||||
this.tcpPort = tcpPort
|
||||
|
||||
// Save the port to the preferences
|
||||
with (preferences!!.edit()) {
|
||||
putString(TCP_PORT_PREF_KEY, tcpPort.toString())
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHostConnectionState(connected: Boolean) {
|
||||
hostConnected = connected
|
||||
}
|
||||
|
||||
override fun onHciPacketCountChange(
|
||||
commandPacketsReceived: Int,
|
||||
aclPacketsReceived: Int,
|
||||
scoPacketsReceived: Int,
|
||||
eventPacketsSent: Int,
|
||||
aclPacketsSent: Int,
|
||||
scoPacketsSent: Int
|
||||
) {
|
||||
hciCommandPacketsReceived = commandPacketsReceived
|
||||
hciAclPacketsReceived = aclPacketsReceived
|
||||
hciScoPacketsReceived = scoPacketsReceived
|
||||
hciEventPacketsSent = eventPacketsSent
|
||||
hciAclPacketsSent = aclPacketsSent
|
||||
hciScoPacketsSent = scoPacketsSent
|
||||
|
||||
}
|
||||
|
||||
override fun onMessage(message: String) {
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val log = Logger.getLogger(MainActivity::class.java.name)
|
||||
private val appViewModel = AppViewModel()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
appViewModel.loadPreferences(getPreferences(Context.MODE_PRIVATE))
|
||||
|
||||
val tcpPort = intent.getIntExtra("port", -1)
|
||||
if (tcpPort >= 0) {
|
||||
appViewModel.tcpPort = tcpPport
|
||||
}
|
||||
|
||||
setContent {
|
||||
MainView(appViewModel, ::startProxy)
|
||||
}
|
||||
|
||||
if (intent.getBooleanExtra("autostart", false)) {
|
||||
startProxy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startProxy() {
|
||||
// Run the proxy in a thread.
|
||||
appViewModel.message = ""
|
||||
thread {
|
||||
log.info("HCI Proxy thread starting")
|
||||
appViewModel.canStart = false
|
||||
try {
|
||||
val hciProxy = HciProxy(appViewModel.tcpPort, appViewModel)
|
||||
hciProxy.run()
|
||||
} catch (error: IOException) {
|
||||
log.warning("Exception while running HCI Server: $error")
|
||||
} catch (error: HalException) {
|
||||
log.warning("HAL exception: ${error.message}")
|
||||
appViewModel.message = "Cannot bind to HAL (${error.message}). You may need to use the command 'setenforce 0' in a root adb shell."
|
||||
}
|
||||
log.info("HCI Proxy thread ended")
|
||||
appViewModel.canStart = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButton(text: String, onClick: () -> Unit, enabled: Boolean) {
|
||||
Button(onClick = onClick, enabled = enabled) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun MainView(appViewModel: AppViewModel, startProxy: () -> Unit) {
|
||||
RemoteHCITheme {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
Text(
|
||||
text = "Bumble Remote HCI",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Divider()
|
||||
Text(
|
||||
text = appViewModel.message
|
||||
)
|
||||
Divider()
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
TextField(
|
||||
label = {
|
||||
Text(text = "TCP Port")
|
||||
},
|
||||
value = appViewModel.tcpPort.toString(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
|
||||
onValueChange = {
|
||||
if (it.isNotEmpty()) {
|
||||
val tcpPort = it.toIntOrNull()
|
||||
if (tcpPort != null) {
|
||||
appViewModel.updateTcpPort(tcpPort)
|
||||
}
|
||||
}
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {keyboardController?.hide()}
|
||||
)
|
||||
)
|
||||
Divider()
|
||||
val connectState = if (appViewModel.hostConnected) "CONNECTED" else "DISCONNECTED"
|
||||
Text(
|
||||
text = "HOST: $connectState",
|
||||
modifier = Modifier.background(color = if (appViewModel.hostConnected) Color.Green else Color.Red),
|
||||
color = Color.Black
|
||||
)
|
||||
Divider()
|
||||
Text(
|
||||
text = "Command Packets Received: ${appViewModel.hciCommandPacketsReceived}"
|
||||
)
|
||||
Text(
|
||||
text = "ACL Packets Received: ${appViewModel.hciAclPacketsReceived}"
|
||||
)
|
||||
Text(
|
||||
text = "SCO Packets Received: ${appViewModel.hciScoPacketsReceived}"
|
||||
)
|
||||
Text(
|
||||
text = "Event Packets Sent: ${appViewModel.hciEventPacketsSent}"
|
||||
)
|
||||
Text(
|
||||
text = "ACL Packets Sent: ${appViewModel.hciAclPacketsSent}"
|
||||
)
|
||||
Text(
|
||||
text = "SCO Packets Sent: ${appViewModel.hciScoPacketsSent}"
|
||||
)
|
||||
Divider()
|
||||
ActionButton(
|
||||
text = "Start", onClick = startProxy, enabled = appViewModel.canStart
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.github.google.bumble.remotehci.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user