Reputation: 2555
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 CBL2CAPChannel
s 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