CoreBluetooth Advertisement Name
Describe the bug When advertising a program in bluetooth peripheral mode, the local name is not broadcast when using pyobj vs Objective-C.
CBPeripheralManager startAdvertising takes a dictionary as it's argument. One of the values is CBAdvertisementDataLocalNameKey. Calling this message from Objective-C works, allowing surrounding devices that are scanning for peripherals to identify the device name as the given to the function. When doing the same from pyobjc, all bluetooth capabilities seem to work except for the name of the device. Wondering what the best way to trouble shoot this would be.
Platform information
- Python version: 3.8.8
- How was python installed (python.org, anaconda, homebrew, ...): conda environment (.e.g., `conda create -n x python=3.8)
- macOS version: 12.3.1
To Reproduce
#import <Foundation/Foundation.h>
#include "peripheral.h"
@implementation Peripheral
- (Peripheral*) init {
self = [super init];
self->service_name = @"BLE";
self->service_uuid_str = @"A07498CA-AD5B-474E-940D-16F1FBE7E8CD";
self->characteristic_uuid_str = @"51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B";
NSLog(@"Starting peripheral manager...")
self->peripheral_manager = [[CBPeripheralManager alloc] initWithDelegate:self queue:dispatch_queue_create("BLE", DISPATCH_QUEUE_SERIAL)];
return self;
}
- (void) add_service {
CBUUID* service_uuid = [CBUUID UUIDWithString:self->service_uuid_str];
CBUUID* characteristic_uuid = [CBUUID UUIDWithString:self->characteristic_uuid_str];
CBCharacteristicProperties props = CBCharacteristicPropertyWrite |
CBCharacteristicPropertyRead |
CBCharacteristicPropertyNotify;
self->characteristic = [[CBMutableCharacteristic alloc] initWithType:characteristic_uuid
properties:props
value:nil
permissions:CBAttributePermissionsWriteable | CBAttributePermissionsReadable
];
self->service = [[CBMutableService alloc] initWithType:service_uuid
primary:true
];
self->service.characteristics = @[self->characteristic];
[self->peripheral_manager addService:self->service];
}
- (void) start_advertising {
if ([self->peripheral_manager isAdvertising]) {
[self->peripheral_manager stopAdvertising];
}
NSDictionary* advertisement = @{
CBAdvertisementDataServiceUUIDsKey: @[self->service.UUID],
CBAdvertisementDataLocalNameKey: self->service_name
};
[self->peripheral_manager startAdvertising:advertisement];
}
- (void) checkOn {
NSLog(@"%ld", (long)self->peripheral_manager.state);
}
- (void) peripheralManager:(CBPeripheralManager*)peripheral
didAddService:(CBService*)service
error:(NSError*)error {
if (error != nil) {
NSLog(@"NO! There's an error!! %@", error);
return;
}
NSLog(@"Service added");
[self start_advertising];
}
- (void)peripheralManagerDidUpdateState:(nonnull CBPeripheralManager *)peripheral {
NSLog(@"State Updated");
[self add_service];
return;
}
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager*)peripheral
error:(NSError*)error {
if (error != nil) {
NSLog(@"NO! There was an error when attempting to advertise!\n%@", error);
}
NSLog(@"Advertising...");
}
- (void) peripheralManager:(CBPeripheralManager*)peripheral
didReceiveReadRequest:(CBATTRequest*)request {
NSLog(@"Request recieved");
if (request.characteristic.UUID == self->characteristic.UUID) {
NSLog(@"Characteristic match");
NSData* data = [@"Hello" dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
if (self->characteristic.value == nil) {
self->characteristic.value = data;
}
if (request.offset > self->characteristic.value.length) {
[self->peripheral_manager respondToRequest:request
withResult:CBATTErrorInvalidOffset
];
}
request.value = data;
[self->peripheral_manager respondToRequest: request
withResult:CBATTErrorSuccess
];
self->characteristic.value = data;
}
}
- (void) peripheralManager:(CBPeripheralManager*)peripheral
didReceiveWriteRequests:(NSArray<CBATTRequest*>*)requests {
NSLog(@"Recieved write request");
for (int i = 0; i < requests.count; i++) {
CBATTRequest* request = requests[i];
NSString* value = [[NSString alloc] initWithData:request.value
encoding:NSUTF8StringEncoding
];
NSLog(@"Someone wants to set the data to: %@", value);
}
[self->peripheral_manager respondToRequest:requests[0]
withResult:CBATTErrorSuccess
];
}
@end
PyObjc version
import objc
from Foundation import NSObject, NSString, NSUTF8StringEncoding, NSDictionary
from CoreBluetooth import (
CBUUID,
CBMutableService,
CBPeripheralManager,
CBATTErrorSuccess,
CBATTErrorInvalidOffset,
CBMutableCharacteristic,
CBCharacteristicPropertyRead,
CBCharacteristicPropertyWrite,
CBCharacteristicPropertyNotify,
CBAttributePermissionsReadable,
CBAttributePermissionsWriteable,
CBAdvertisementDataLocalNameKey,
CBAdvertisementDataServiceUUIDsKey,
)
from libdispatch import dispatch_queue_create, DISPATCH_QUEUE_SERIAL
CBPeripheralManagerDelegate = objc.protocolNamed("CBPeripheralManagerDelegate")
class PyPeripheral(NSObject, protocols=[CBPeripheralManagerDelegate]):
def init(self):
self = objc.super(PyPeripheral, self).init()
if self is None:
return None
self.service_name = "PyBLE"
self.service_uuid_str = "A07498CA-AD5B-474E-940D-16F1FBE7E8CD"
self.characteristic_uuid_str = "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B"
print("Starting peripheral manager...")
self.peripheral_manager = CBPeripheralManager.alloc().initWithDelegate_queue_(
self, dispatch_queue_create(b"BLE", DISPATCH_QUEUE_SERIAL)
)
return self
def add_service(self):
service_uuid = CBUUID.UUIDWithString_(
NSString.stringWithString_(self.service_uuid_str)
)
characteristic_uuid = CBUUID.UUIDWithString_(
NSString.stringWithString_(self.characteristic_uuid_str)
)
props = (
CBCharacteristicPropertyWrite
| CBCharacteristicPropertyRead
| CBCharacteristicPropertyNotify
)
self.characteristic = (
CBMutableCharacteristic.alloc().initWithType_properties_value_permissions_(
characteristic_uuid,
props,
None,
CBAttributePermissionsWriteable | CBAttributePermissionsReadable,
)
)
self.service = CBMutableService.alloc().initWithType_primary_(
service_uuid, True
)
self.service.setCharacteristics_([self.characteristic])
self.peripheral_manager.addService_(self.service)
def start_advertising(self):
if self.peripheral_manager.isAdvertising():
self.peripheral_manager.stopAdvertising()
advertisement = {
CBAdvertisementDataServiceUUIDsKey: [self.service.UUID()],
CBAdvertisementDataLocalNameKey: self.service_name,
}
self.peripheral_manager.startAdvertising_(advertisement)
def checkOn(self): # noqa: N802
print(self.peripheral_manager.state)
def peripheralManager_didAddService_error_( # noqa: N802
self, peripheral, service, error
):
if error is not None:
print(f"NO! There's an error!! {error}")
return
print("Service added")
self.start_advertising()
def peripheralManagerDidUpdateState_(self, peripheral): # noqa: N802
print(f"State Updated: {peripheral.state()}")
self.add_service()
return
def peripheralManagerDidStartAdvertising_error_( # noqa: N802
self, peripheral, error
):
if error is not None:
print(f"NO! There was an error when attempting to advertise!\n{error}")
print("Advertising...")
def peripheralManager_didReceiveReadRequest_( # noqa: N802
self, peripheral, request
):
print("Request recieved")
if request.characteristic().UUID() == self.characteristic.UUID():
print("Characteristic match")
value_str = NSString.stringWithString_("Hello")
data = value_str.dataUsingEncoding_allowLossyConversion_(
NSUTF8StringEncoding, False
)
if self.characteristic.value() is None:
self.characteristic.setValue_(data)
if request.offset() > self.characteristic.value().length():
self.peripheral_manager.respondToRequest_withResult_(
request, CBATTErrorInvalidOffset
)
request.setValue_(data)
self.peripheral_manager.respondToRequest_withResult_(
request, CBATTErrorSuccess
)
self.characteristic.setValue_(data)
def peripheralManager_didReceiveWriteRequests_( # noqa: N802
self, peripheral, requests
):
print("Recieved write request")
for request in requests:
value = NSString.alloc().initWithData_encoding_(
request.value, NSUTF8StringEncoding
)
print(f"Someone wants to set the data to: {value}")
self.peripheral_manager.respondToRequest_withResult_(
requests[0], CBATTErrorSuccess
)
Happy to edit this and attach as files to decrease verbosity
Expected behavior
A call to [[Peripheral alloc] init] in objective-C or PyPeripheral.alloc().init() in python will start advertising and should advertise the name as given
Additional context
I've started testing by mixing these two codes using python extensions. For example, building the peripheral file into an object file that I can then turn into a python extension and call from a __main__ file still works. It seems at this point that name advertising begins to fail when either the CBPeripheralManager or the CBPeripheralManagerDelegate are initialized in pyobjc. There could be more at play but not sure.
Sorry about the slow response, I've been travelling a bit (first time in 2 years), and am working on updates for the macOS 13 SDK.
The code samples you provided should be enough to reproduce the issue on my end. I expect to get around to this either next weekend.
I'm finally getting around to looking into this, and notice that I'm missing some information on how to reproduce this. In particular: how do I scan for a peripheral? With both the ObjC and Python variants I don't see the advertised device when using the Bluetooth settings pane on an iOS device.
I recommend using the nRF Connect app.
On macOS Sonoma I do get an advertised name when adding the following to the end of the python script to get a complete program:
from Cocoa import NSRunLoop
perifical = PyPeripheral.alloc().init()
loop = NSRunLoop.currentRunLoop()
loop.run()
That said, actually connecting using the 'nRF Connect' app on my iPhone doesn't work (failed to encrypt the connection).
And a different app ("LightBlue") that's used in a tutorial about CoreBluetooth doesn't see the name.
I'm never used CoreBluetooth myself, and to be honest I'm not sure how to debug this and if the connection error is to be expected. I did notice that I get different results in the iPhone app when I change the UUIDs. In particular, I changed the service and characteristics uuid to ones mentioned in this stackoverflow question and that changed how the service and characteristic are formatted. Sadly, still no connection possible.
UPDATE: I get the same result when I insert the Python code into a simple GUI program with the app bundle created using py2app.
On macOS Sonoma I do get an advertised name when adding the following to the end of the python script to get a complete program:
When I follow this guide and run the previous code, I can find it on the iPhone. But I encountered another problem:
self.service_name = "PyBLE-test"
When the length of my service_name exceeds 8, the device cannot be discovered.