Torsten Robitzki
Torsten Robitzki

Reputation: 2555

Incoming BLE L2CAP SDU not reported to Delegate

I have an application, running on macOS, that communicates with a Bluetooth LE peripheral. I'm able to establish an L2CAP channel and after the channel is established, the peripheral sends a first (quite small) SDU on that channel to the application. The PSM is 0x80 and was chosen by the peripheral's BLE stack. The application receives the PSM via GATT.

I can see the SDU being sent in a single LL PDU with Wireshark:

Frame 569: 44 bytes on wire (352 bits), 44 bytes captured (352 bits) on interface /dev/cu.usbmodem0006832334881-4.2, id 0
nRF Sniffer for Bluetooth LE
Bluetooth Low Energy Link Layer
    Access Address: 0x50657518
    [Master Address: Apple_7a:a5:d6 (f4:d4:88:7a:a5:d6)]
    [Slave Address: c4:ed:78:ac:5b:31 (c4:ed:78:ac:5b:31)]
    Data Header
        .... ..10 = LLID: Start of an L2CAP message or a complete L2CAP message with no fragmentation (0x2)
        .... .0.. = Next Expected Sequence Number: 0 [ACK]
        .... 1... = Sequence Number: 1 [OK]
        ...1 .... = More Data: True
        ..0. .... = CTE Info: Not Present
        00.. .... = RFU: 0
        Length: 18
    [L2CAP Index: 30]
    [Connection Parameters in: 473]
    CRC: 0x9850ef
Bluetooth L2CAP Protocol
    Length: 14
    CID: Dynamically Allocated Channel (0x004e)
    [Connect in frame: 562]
    [PSM: Unknown (0x0080)]
    SDU Length: 12
    Payload: 000509040401053c4c6f7600

I can also see the SDU being received in Apples PacketLogger. But I miss the corresponding call to a stream event handler.

The same BLE code ran in a different project without problems on MacOS and iOS. Now, I moved the code to another project. Discovery and GATT communication works without any problem, yet the reception of L2CAP messages causes a problem.

The code creates a dispatch queue and passes it to the CBCentralManager:

dispatch_queue  = dispatch_queue_create("de.torrox.ble_event_queue", NULL);
manager         = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_queue options:nil];

When the L2CAP channel is established, the didOpenL2CAPChannel callback gets called from a thread within the dispatch_queue (has been verified with lldb):

- (void)peripheral:(CBPeripheral *)peripheral
didOpenL2CAPChannel:(CBL2CAPChannel *)channel
             error:(NSError *)error
{
    [channel inputStream].delegate = self;
    [channel outputStream].delegate = self;
    [[channel inputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [[channel outputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [[channel inputStream] open];
    [[channel outputStream] open];

    ...
    //A reference to the channel is stored in the outside channel object
    [channel retain];
    ...
}

Yet, not a single stream event is generated:

- (void)stream:(NSStream *)stream
   handleEvent:(NSStreamEvent)event_code
{
    Log( @"stream:handleEvent %@, %lu", stream, event_code );
    ...
}

What could be the problem here? How could I get further information to track this down? Is there a chance, that received data is not stored until it is picked up by a call to read?

Edit: From the Wireshark trace, I got that besides the L2CAP SDU, there was a second PDU (a GATT Notification) that was transported on the very same connection event. To make sure, that this does not cause any problems, I removed the following GATT Notification, but still, the L2CAP SDU does not reach the application.

Edit: To test whether it's only the event that is missing or the data at all, I added another test for hasBytesAvailable to the CBL2CAPChannels inputStream. The result is, that the function still returns false. I've also logged the streamStatus, which is NSStreamStatusOpen (2), and streamError, which is null.

Edit-Edit: If I force the BLE connection to be encrypted, a subsequent call to hasBytesAvailable yields the expected amount of data. But still, the callback has not been called.

Edit: I added a breakpoint to the didOpenL2CAPChannel function to see, which thread is calling that callback. That thread is not the main thread, but a thread labeled with queue = 'de.torrox.ble_event_queue'. If I break into the debugger later, I see that thread blocking on libsystem_kernel.dylib__workq_kernreturn + 8`

Edit: The application is a C++ command line application build with Apple Clang. Only the BLE-related stuff is written in Objective C to provide a C interface to the C++ part of the application.

Edit: The main thread of execution is usually blocking on a boost::asio::io_context::run() call. The design is, to have the stream callback stream:handleEvent to post callback invocations on that io_context, and thus to wake up the main thread and get that callbacks being invoked on the main thread.

Edit: What puzzles me most, is that all GATT-related events are reported by calls to the delegate callbacks. Only the L2CAP stream-related callbacks are not called. For the inputStream:scheduleInRunLoop:forMode call, there has to be a mode parameter. If I got that wrong, because the given mode, is neither the current mode nor a common mode, the data would be received but most likely not reported.

Edit: When I request the current mode of the currentRunLoop ([NSRunLoop currentRunLoop].currentMode), I get (null). When I pass that very same value to scheduleInRunLoop:forMode, (forMode:(NSString *)[NSNull null]), the behavior stays the same.

Upvotes: 0

Views: 121

Answers (0)

Related Questions