ronaldoussoren / pyobjc

The Python <-> Objective-C Bridge with bindings for macOS frameworks
https://pyobjc.readthedocs.io
575 stars 47 forks source link

CoreBluetooth Advertisement Name #474

Open kevincar opened 2 years ago

kevincar commented 2 years ago

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

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.

ronaldoussoren commented 2 years ago

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.

ronaldoussoren commented 1 year ago

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.

dlech commented 1 year ago

I recommend using the nRF Connect app.

ronaldoussoren commented 1 year ago

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.

wwj718 commented 1 year ago

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.