I'm confused on how long the value of a characteristic for CBMutableCharacteristic can be. If I have an archived array of objects, can I set the value of the Characteristic to this archived array? Or am I better off having a separate characteristic for each archived object in the array?
The specification limits the maximum length of a characteristic value in 512 octets (Bluetooth specification V4.0 Vol 3. Part F 3.2.9). On the central side you start a read request with the readValueForCharacteristic: API. Then on the peripheral side you receive the corresponding callback:
CBMutableCharacteristic *characteristic = // the characteristic with long data
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {
// Ensure offset is in the valid range
if (request.offset > characteristic.value.length) {
// respond with error
[self.peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
return;
}
NSRange range = NSMakeRange(request.offset, characteristic.value.length - request.offset);
request.value = [characteristic.value subdataWithRange:range];
[self.peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
}
The callback will be called with increasing offset values as long as the response is not less than the Maximum Transferrable Unit (MTU). The MTU is negotiated by the system and you have no way to query it in advance. For this reason the range is set up to stretch all the way to the data length so the system can decide how much of the data it is going to send to the central. By default it is 20 but iOS 7 has some tweaks that enable greater sizes. Check out the WWDC videos for more detail.
With this info you should be able to figure out a way to expose your data. Keep in mind that the more services and characteristics you have, the longer the discovery will take. Using long reads and long writes is just a convenience.
This project may be exactly what you're looking for: SimpleShare
It uses Bluetooth LE to share arrays of strings between devices. The technique is to use just one characteristic to share the data, but when a central subscribes to the characteristic, the peripheral changes the value to each new piece of data it wants to send, until the entire too-long message is sent. Basically, it breaks it down into bite-sized chunks that fit in the characteristic and sends them one at a time.
Hope that helps!
Related
I'm trying to send data over BLE from my iPhone to an ESP32 board. I'm developing in flutter platform and I'm using flutter_reactive_ble library.
My iPhone can connect to the other device and it can also send 1 byte using writeCharacterisiticWithResponse function. But when I try to send my real data which is large (>7000 bytes), it then gives me the error:
flutter: Error occured when writing 9f714672-888c-4450-845f-602c1331cdeb :
Exception: GenericFailure<WriteCharacteristicFailure>(
code: WriteCharacteristicFailure.unknown,
message: "Error Domain=CBATTErrorDomain Code=17
"Resources are insufficient."
UserInfo={NSLocalizedDescription=Resources are insufficient.}")
I tried searching for this error but didn't find additional info, even in Apple Developer website. It just says:
Resources are insufficient to complete the ATT request.
What does this error really means? Which resources are not sufficient and how to work around this problem?
This is almost certainly larger than this characteristic's maximum value length (which is probably on the order of 10s of bytes, not 1000s of bytes). Before writing, you need to call maximumWriteValueLength(for:) to see how much data can be written. If you're trying to send serial data over a characteristic (which is common, but not really what they were designed for), you'll need to break your data up into chunks and reassemble them on the peripheral. You will likely either need an "end" indicator of some kind, or you will need to send the length of the payload first so that the receiver knows how much to excpect.
First of all, a characteristic value cannot be larger than 512 bytes. This is set by the ATT standard (Bluetooth Core Specification v5.3, Vol 3, Part F (ATT), section 3.2.9). This number has been set arbitrarily by the protocol designers and does not map to any technical limitation of the protocol.
So, don't send 7000 bytes in a single write. You need to keep it at most 512 to be standard compliant.
If you say that it works with another Bluetooth stack running on the GATT server, then I guess CoreBluetooth does not enforce/check the maximum length of 512 bytes on the client side (I haven't tested). Therefore I also guess the error code you see was sent by the remote device rather than by CoreBluetooth locally as a pre-check.
There are three different common ways of writing a characteristic on the protocol level (Bluetooth Core Specification v5.3, Vol 3, Part G (GATT), section 4.9 Characteristic Value Write):
Write Without Response (4.9.1)
Write Characteristic Value (4.9.3)
Write Long Characteristic Values (4.9.4)
Number one is unidirectional and does not result in a response packet. It uses a single ATT_WRITE_CMD packet where the value must be at most ATT_MTU-3 bytes in length. This length can be retrieved using maximumWriteValueLength(for:) with .withoutResponse. The requestMtu method in flutter_reactive_ble uses this method internally. If you execute many writes of this type rapidly, be sure to add flow control to avoid CoreBluetooth dropping outgoing packets before they are sent. This can be done through peripheralIsReadyToSendWriteWithoutResponse by simply always waiting for this callback after each write, before you write the next packet. Unfortunately, it seems flutter_reactive_ble does not implement this flow control mechanism.
Number two uses a single ATT_WRITE_REQ where the value must be at most ATT_MTU-3 bytes in length, just as above. Use the same approach as above to retrieve that maximum length (note that maximumWriteValueLength with .withResponse always returns 512 and is not what you want). Here however, either an ATT_WRITE_RSP will be returned on success or an error packet will be received with an error code. Only one ATT transaction can be outstanding at a time, which significantly lowers throughput compared to Write Without Response.
Number three uses a sequence of multiple ATT_PREPARE_WRITE_REQ packets (containing offset and value) followed by an ATT_EXECUTE_WRITE_REQ. The maximum length of the value in each each chunk is ATT_MTU-5. Each _REQ packet also requires a corresponding _RSP packet before it can continue (alternatively, an error code could be sent by the remote device). This approach is used when the characteristic value to be written is too long to be sent using a single ATT_WRITE_REQ.
For any of the above write methods, you are always also limited by the maximum attribute size of 512 bytes as per the specification.
Any Bluetooth stack I know of transparently chooses between "Write Characteristic Value" and "Write Long Characteristic Values" when you tell it to write with response, depending on the value length and MTU. Server side it's a bit different. Some stacks put the burden on the user to combine all packets but it seems nimble handles that on its own. From what I can see in the source code (https://github.com/apache/mynewt-nimble/blob/26ccb8af1f3ea6ad81d5d7cbb762747c6e06a24b/nimble/host/src/ble_att_svr.c#L2099) it can return the "Insufficient Resources" error code when it tries to allocate memory but fails (most likely due to too much buffered data). This is what might happen for you. To answer your first actual question, the standard itself does not say anything else about this error code than simply "Insufficient Resources to complete the request".
The error has nothing to do with LE Data Length extension, which is simply an optimization for a lower layer (BLE Link Layer) that does not affect the functionality of the host stack. The L2CAP layer will take care of the reassembling of smaller link layer packets if necessary, and must always support up to the negotiated MTU without overflowing any buffers.
Now, to answer your second question, if you send very large amounts of data (7000 bytes), you must divide the data in multiple chunks and come up with a way to correctly be able to combine them. Each chunk is written as a full characteristic value. When you do this, be sure to send values at most of size ATT_MTU-3 (but never larger than 512 bytes), to avoid the inefficient overheads of "Write Long Characteristic Values". It's then up to your application code to make sure you don't run out of memory in case too much data is sent.
Is there any way to dynamically sniff out the data length of a CBCharacteristic?
I'm evolving the characteristic API on a gadget I have, and I want to change the field width of one of the characteristics. If I make the change in the iOS side so that it writes a value of 9 bytes instead of 7 bytes, a device that has the old API (characteristic field width of 7) on it causes the following to occur when I attempt to write the value:
Error Domain=CBATTErrorDomain Code=13 "The value's length is invalid." UserInfo={NSLocalizedDescription=The value's length is invalid.}
What I'd like to determine is whether or not I can catch that error, and then somehow query the CBCharacteristic what its current field width is. Obviously at some point, it determined that 9 > 7 and caused the error to bubble up. I've tried reading the descriptor associated with the characteristic, but there's just one descriptor (<CBDescriptor: 0x2830cbe40, UUID = Client Characteristic Configuration, value = 2>) which doesn't tell me much.
This is a standard ATT protocol error that's generated by the peripheral, not iOS. It means you sent a value and the peripheral responded that the value was the wrong length. Characteristics do not necessarily have specific lengths. The correct length might be context dependent. For example, a characteristic might accept 2 bytes, 4 bytes, or 8 bytes. If you write 7 bytes, it could return this error. Or the first byte might be a command selector, and the rest might be the payload that needs to be of a specific length for that command. There's no simple "what's the length of this characteristic" for these designs.
There's no general way to know what a remote service wants. You need to know that through API documentation. This is the equivalent of getting back a 405 Method not allowed error from an HTTP server. It's up to you to know what methods are allowed. A particular system might give you a way to query it, and it might not. It's not part of the HTTP spec to provide that, and similarly it's not part of the BLE spec.
You can query the maximum packet length allowed by a characteristic with
peripheral.maximumWriteValueLength(for:CBCharacteristicWriteType.<>)
I want to check that can we advertise simple string like "ttfgpV5hm8Z4mMlD" at the time of peripheral advertising I'm trying to pass this string into CBUUID string but whenever central scan for peripheral it gives an error like _C.CBUUID.CreationError.invalidString: I write down code which I use in peripheral and central.
Peripheral side :
Peripheral(configuration: configuration, advertisementData: [.localName("Test"), .servicesUUIDs("ttfgpV5hm8Z4mMlD")])
Central side :
let configuration = try! Configuration(services: [service], advertisement: "ttfgpV5hm8Z4mMlD")
please give me a guideline for how to pass a simple string at the time of scanning and advertising.
CBUUIDs are UUIDs that contain hexadecimal digits - they aren't arbitrary strings. So unless your "simple string" only contains hex digits and you don't mind handling the hyphen separators, then you can't. What you can do is include the string as a value of a characteristic that is included by the service.
I am trying to design an app that read several temperatures. Is it true that one characteristic UUID can only handle 1 value? If so, how can I read multiple values?(For instance, temperature 1, temperature 2, temperature 3...) Do I need to declare multiple characteristic UUIDs myself? But if I only declare those in my app, how would the peripheral know what UUID corresponds to what value then? Most of the example only read one value (temperature, heart rate,etc)
You can get different values from the same characteristic. For that, you may have to write different values to the write characteristic. For example, say your write characteristic is A and your read characteristic B.
You can write a value to A like this
[self.discoveredPeripheral writeValue:data
forCharacteristic:characteristic
type:CBCharacteristicWriteWithResponse];
where data varies as per the request that you would like to make.
For your read characteristic B, the indication property should be set to YES as follows in the didDiscoverCharacteristicsForService delegate callback
if (c.properties & CBCharacteristicPropertyIndicate) {
[peripheral setNotifyValue:YES forCharacteristic:c];
}
Now whenever you write a value to A, you will get a callback on the delegate method didUpdateValueForCharacteristic. You will have to handle the response properly.
Of course, for all of this to work, your ble device should be programmed accordingly.
Note that the code is written in Objective-C
How can I send 28 bytes of peripheral name-key ad data and receive it by the central?
Normally, with the UUID, there's only room for 8 characters in the peripheral name key ad data.
But there's this TRICK:
I discovered if my name key has 28 characters, the central ad data is empty, BUT all 28 characters all appear one time in the central's didDiscoverPeripheral's peripherl.name variable. But the next time the central scans same device, it gets instead the GATT value of "iPad" (because my central and peripheral are on two iPad minis.)
Apple Doc. says it normally sends the GATT, but I observe that it always sends the 28-char name once.
BACKGROUND:
I want to put identification data in the peripheral's ad data, to avoid a read-value interchange. The reason is to have the shortest communication interchange between devices, to save time and battery.
I've tried turning scanning off then back on, and stopping and re-starting advertising. Same problem.
This is unfortunately not going to work. Once you connect, the device name characteristic is read by CoreBluetooth and it will be used as the name for the peripheral later on. The only place the name you set will always appear is the advertisement data:
(lldb)po advertisementData
{
kCBAdvDataChannel = 37;
kCBAdvDataIsConnectable = 1;
kCBAdvDataLocalName = MyPeripheral;
kCBAdvDataServiceUUIDs = (
"Unknown (<e20a39f4 73f54bc4 a12f17d1 ad07a961>)"
);
}
The device name characteristic cannot be accessed in an iOS device in any way.
It may be that the Bluetooth LE controller has a parameter set to filter duplicates. You may need HCI layer access to change this if you don't have API calls to the HCI "set scan enable" command. See Bluetooth Core Specification v4.0 Vol 2 Part E 7.8.11