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