• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2025 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from bumble.core import (
16    BT_OBEX_OBJECT_PUSH_SERVICE,
17    BT_L2CAP_PROTOCOL_ID,
18    BT_RFCOMM_PROTOCOL_ID,
19    BT_OBEX_PROTOCOL_ID,
20)
21from bumble.device import Device
22from bumble.l2cap import ClassicChannelSpec
23from bumble.pandora import utils
24from bumble.rfcomm import Server
25from bumble.sdp import (
26    DataElement,
27    ServiceAttribute,
28    SDP_PUBLIC_BROWSE_ROOT,
29    SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
30    SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
31    SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
32    SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
33    SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
34)
35from google.protobuf.empty_pb2 import Empty
36import grpc
37import logging
38from pandora_experimental.opp_grpc_aio import OppServicer
39from pandora_experimental.opp_pb2 import AcceptPutOperationResponse
40
41OBEX_RFCOMM_CHANNEL = 0x07
42
43SUPPORTED_FORMAT_VCARD_2_1 = 0x01
44SUPPORTED_FORMAT_VCARD_3_0 = 0x02
45SUPPORTED_FORMAT_VCAL_1_0 = 0x03
46SUPPORTED_FORMAT_VCAL_2_0 = 0x04
47SUPPORTED_FORMAT_VNOTE = 0x05
48SUPPORTED_FORMAT_VMESSAGE = 0x06
49SUPPORTED_FORMAT_ANY = 0xFF
50
51BT_OBEX_OBJECT_PUSH_SERVICE_VERSION = 0x0102
52
53OPP_GOEM_L2CAP_PSM_ATTRIBUTE_ID = 0x0200
54OPP_SUPPORTED_FORMATS_LIST_ATTRIBUTE_ID = 0x0303
55
56# See Bluetooth spec @ Vol 3, Part B - 5.1.1. ServiceRecordHandle attribute
57SDP_SERVICE_RECORD_HANDLE_NON_RESERVED_START = 0x00010000
58SDP_SERVICE_RECORD_HANDLE_NON_RESERVED_END = 0xFFFFFFFF
59
60
61def find_free_sdp_record_handle(device, start=SDP_SERVICE_RECORD_HANDLE_NON_RESERVED_START):
62    for candidate_handle in range(start, SDP_SERVICE_RECORD_HANDLE_NON_RESERVED_END):
63        if candidate_handle not in device.sdp_service_records:
64            return candidate_handle
65    raise RuntimeError("No available sdp record handles!")
66
67
68# See Bluetooth Object Push Profile Spec v1.1 - 6.1 SDP Service Records
69def sdp_records(device, l2cap_psm, rfcomm_channel):
70    service_record_handle = find_free_sdp_record_handle(device)
71    return {
72        service_record_handle: [
73            ServiceAttribute(
74                SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
75                DataElement.unsigned_integer_32(service_record_handle),
76            ),
77            ServiceAttribute(
78                SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
79                DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
80            ),
81            ServiceAttribute(
82                SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
83                DataElement.sequence([DataElement.uuid(BT_OBEX_OBJECT_PUSH_SERVICE)]),
84            ),
85            ServiceAttribute(
86                SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
87                DataElement.sequence([
88                    DataElement.sequence([
89                        DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
90                    ]),
91                    DataElement.sequence(
92                        [DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
93                         DataElement.unsigned_integer_8(rfcomm_channel)]),
94                    DataElement.sequence([
95                        DataElement.uuid(BT_OBEX_PROTOCOL_ID),
96                    ]),
97                ]),
98            ),
99            ServiceAttribute(
100                SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
101                DataElement.sequence([
102                    DataElement.sequence([
103                        DataElement.uuid(BT_OBEX_OBJECT_PUSH_SERVICE),
104                        DataElement.unsigned_integer_16(BT_OBEX_OBJECT_PUSH_SERVICE_VERSION),
105                    ]),
106                ]),
107            ),
108            ServiceAttribute(
109                OPP_SUPPORTED_FORMATS_LIST_ATTRIBUTE_ID,
110                DataElement.sequence([
111                    DataElement.unsigned_integer_8(SUPPORTED_FORMAT_VCARD_2_1),
112                    DataElement.unsigned_integer_8(SUPPORTED_FORMAT_VCARD_3_0),
113                    DataElement.unsigned_integer_8(SUPPORTED_FORMAT_VCAL_1_0),
114                    DataElement.unsigned_integer_8(SUPPORTED_FORMAT_VCAL_2_0),
115                    DataElement.unsigned_integer_8(SUPPORTED_FORMAT_VNOTE),
116                    DataElement.unsigned_integer_8(SUPPORTED_FORMAT_VMESSAGE),
117                    DataElement.unsigned_integer_8(SUPPORTED_FORMAT_ANY),
118                ]),
119            ),
120            ServiceAttribute(
121                OPP_GOEM_L2CAP_PSM_ATTRIBUTE_ID,
122                DataElement.unsigned_integer_16(l2cap_psm),
123            ),
124        ]
125    }
126
127
128# This class implements the Opp Pandora interface.
129class OppService(OppServicer):
130
131    def __init__(self, device: Device, rfcomm_server: Server) -> None:
132        self.device = device
133        self.l2cap_server = self.device.create_l2cap_server(ClassicChannelSpec())
134        self.rfcomm_server = rfcomm_server
135        self.log = utils.BumbleServerLoggerAdapter(logging.getLogger(), {'service_name': 'opp', 'device': device})
136        self.setup_channel_and_sdp_records()
137
138    def acceptor(self, dlc) -> None:
139        dlc.sink = self.rx_bytes
140
141    def rx_bytes(self, bytes):
142        self.log.debug(f"Received bytes")
143
144    def setup_channel_and_sdp_records(self):
145        rfcomm_channel = self.rfcomm_server.listen(acceptor=self.acceptor)
146        self.device.sdp_service_records.update(sdp_records(self.device, self.l2cap_server.psm, rfcomm_channel))
147
148    @utils.rpc
149    async def AcceptPutOperation(self, request: Empty, context: grpc.ServicerContext) -> AcceptPutOperationResponse:
150        self.log.info(f"AcceptPutOperation")
151        return AcceptPutOperationResponse()
152