Descriptor
Overview
A Descriptor provides additional information and configuration for a characteristic. Descriptors are metadata that describe how to interact with a characteristic or provide extra details about its value. The most common descriptor is the Client Characteristic Configuration Descriptor (CCCD), which controls notifications and indications.
Interface: IBluetoothRemoteDescriptor
What Does It Do?
Descriptors allow you to:
- Enable/disable notifications on a characteristic (via CCCD)
- Get human-readable descriptions of a characteristic
- Read extended properties and format information
- Configure how a characteristic behaves
GATT Hierarchy Position
Device
└── Service
└── Characteristic
└── Descriptor ◄── You are here
Descriptors are the deepest level in the GATT hierarchy.
Standard Descriptors
The Bluetooth SIG defines several standard descriptors:
| Descriptor | UUID | Purpose |
|---|---|---|
| Client Characteristic Configuration (CCCD) | 00002902-0000-1000-8000-00805F9B34FB |
Enable/disable notifications and indications |
| Characteristic User Description | 00002901-0000-1000-8000-00805F9B34FB |
Human-readable text description |
| Characteristic Presentation Format | 00002904-0000-1000-8000-00805F9B34FB |
Data format (units, exponent, etc.) |
| Characteristic Extended Properties | 00002900-0000-1000-8000-00805F9B34FB |
Additional properties |
| Characteristic Aggregate Format | 00002905-0000-1000-8000-00805F9B34FB |
Multiple format descriptors |
Most Common: The CCCD is by far the most commonly used descriptor, as it's required for enabling notifications.
Getting Started
1. Get a Descriptor
Descriptors are typically accessed through characteristics:
await device.ConnectAsync();
await device.ExploreServicesAsync(ServiceExplorationOptions.Full);
var service = device.GetService(serviceUuid);
var characteristic = service.GetCharacteristic(characteristicUuid);
// Descriptors are automatically discovered with Full exploration
var descriptors = characteristic.GetDescriptors();
// Get specific descriptor (e.g., CCCD)
var cccdUuid = Guid.Parse("00002902-0000-1000-8000-00805F9B34FB");
var cccd = characteristic.GetDescriptorOrDefault(cccdUuid);
2. Check Capabilities
if (descriptor.CanRead)
Console.WriteLine("Can read descriptor");
if (descriptor.CanWrite)
Console.WriteLine("Can write descriptor");
3. Read a Descriptor
if (descriptor.CanRead)
{
var value = await descriptor.ReadValueAsync();
Console.WriteLine($"Descriptor value: {BitConverter.ToString(value.ToArray())}");
}
4. Write a Descriptor
if (descriptor.CanWrite)
{
byte[] data = { 0x01, 0x00 }; // Example: Enable notifications
await descriptor.WriteValueAsync(data);
Console.WriteLine("Descriptor written!");
}
Descriptor Properties
Basic Properties
// Descriptor UUID
Guid id = descriptor.Id;
// Descriptor name (human-readable)
string name = descriptor.Name;
// Parent characteristic
IBluetoothRemoteCharacteristic characteristic = descriptor.RemoteCharacteristic;
// Capability checks
bool canRead = descriptor.CanRead;
bool canWrite = descriptor.CanWrite;
Value Properties
Access the current cached value:
// As ReadOnlyMemory<byte>
ReadOnlyMemory<byte> value = descriptor.Value;
// As ReadOnlySpan<byte>
ReadOnlySpan<byte> span = descriptor.ValueSpan;
Common Descriptors
Client Characteristic Configuration Descriptor (CCCD)
The CCCD controls notifications and indications. However, you typically don't interact with it directly - the library handles this automatically when you call StartListeningAsync() on a characteristic:
// High-level API (recommended)
await characteristic.StartListeningAsync(); // Library writes to CCCD automatically
await characteristic.StopListeningAsync(); // Library disables CCCD automatically
// Low-level API (manual CCCD control)
var cccdUuid = Guid.Parse("00002902-0000-1000-8000-00805F9B34FB");
var cccd = characteristic.GetDescriptor(cccdUuid);
// Enable notifications (0x01, 0x00)
await cccd.WriteValueAsync(new byte[] { 0x01, 0x00 });
// Enable indications (0x02, 0x00)
await cccd.WriteValueAsync(new byte[] { 0x02, 0x00 });
// Disable (0x00, 0x00)
await cccd.WriteValueAsync(new byte[] { 0x00, 0x00 });
CCCD Values:
0x00, 0x00: Notifications and indications disabled0x01, 0x00: Notifications enabled0x02, 0x00: Indications enabled0x03, 0x00: Both enabled (rare)
Characteristic User Description
Provides a human-readable text description:
var descriptionUuid = Guid.Parse("00002901-0000-1000-8000-00805F9B34FB");
var descriptor = characteristic.GetDescriptorOrDefault(descriptionUuid);
if (descriptor != null && descriptor.CanRead)
{
var value = await descriptor.ReadValueAsync();
string description = Encoding.UTF8.GetString(value.ToArray());
Console.WriteLine($"Characteristic description: {description}");
}
Characteristic Presentation Format
Describes the format of the characteristic value:
var formatUuid = Guid.Parse("00002904-0000-1000-8000-00805F9B34FB");
var descriptor = characteristic.GetDescriptorOrDefault(formatUuid);
if (descriptor != null && descriptor.CanRead)
{
var value = await descriptor.ReadValueAsync();
var span = value.Span;
// Parse presentation format (7 bytes)
byte format = span[0]; // Data format (e.g., uint8, uint16, float)
byte exponent = span[1]; // Power of 10 exponent
ushort unit = BitConverter.ToUInt16(span.Slice(2, 2).ToArray(), 0); // Units
byte namespaceId = span[4]; // Namespace (1 = Bluetooth SIG)
ushort description = BitConverter.ToUInt16(span.Slice(5, 2).ToArray(), 0);
Console.WriteLine($"Format: {format}, Exponent: {exponent}, Unit: {unit}");
}
Reading Descriptors
Basic Read
var value = await descriptor.ReadValueAsync();
// Access as byte array
byte[] bytes = value.ToArray();
// Access as span
ReadOnlySpan<byte> span = value.Span;
Skip Redundant Reads
// First read: actually reads from device
var value1 = await descriptor.ReadValueAsync();
// Skip if already read
var value2 = await descriptor.ReadValueAsync(skipIfPreviouslyRead: true);
Check Current State
// Is currently performing a read?
bool isReading = descriptor.IsReadingValue;
Writing Descriptors
Basic Write
byte[] data = { 0x01, 0x00 };
await descriptor.WriteValueAsync(data);
Skip Redundant Writes
// Only write if value has changed
await descriptor.WriteValueAsync(
data,
skipIfOldValueMatchesNewValue: true
);
Check Current State
// Is currently performing a write?
bool isWriting = descriptor.IsWritingValue;
Events
Monitor value changes:
descriptor.ValueUpdated += (s, args) =>
{
Console.WriteLine($"Descriptor value changed: {BitConverter.ToString(args.OldValue.ToArray())} -> {BitConverter.ToString(args.NewValue.ToArray())}");
};
Wait for Value Change
// Wait for value to change
var value = await descriptor.WaitForValueChangeAsync();
// Wait for specific value
var value = await descriptor.WaitForValueChangeAsync(
valueFilter: v => v.Span[0] == 0x01,
timeout: TimeSpan.FromSeconds(5)
);
Common Patterns
Read All Descriptors
async Task ReadAllDescriptorsAsync(IBluetoothRemoteCharacteristic characteristic)
{
var descriptors = characteristic.GetDescriptors();
Console.WriteLine($"Found {descriptors.Count} descriptors:");
foreach (var descriptor in descriptors)
{
Console.WriteLine($" {descriptor.Name} ({descriptor.Id})");
if (descriptor.CanRead)
{
try
{
var value = await descriptor.ReadValueAsync();
Console.WriteLine($" Value: {BitConverter.ToString(value.ToArray())}");
}
catch (Exception ex)
{
Console.WriteLine($" Error reading: {ex.Message}");
}
}
}
}
Manually Control CCCD
async Task ManualCccdControlAsync(IBluetoothRemoteCharacteristic characteristic)
{
var cccdUuid = Guid.Parse("00002902-0000-1000-8000-00805F9B34FB");
var cccd = characteristic.GetDescriptorOrDefault(cccdUuid);
if (cccd == null)
{
Console.WriteLine("CCCD not found - notifications not supported");
return;
}
// Subscribe to characteristic value changes
characteristic.ValueUpdated += (s, args) =>
{
Console.WriteLine($"Notification: {BitConverter.ToString(args.Value.ToArray())}");
};
// Enable notifications by writing to CCCD
await cccd.WriteValueAsync(new byte[] { 0x01, 0x00 });
Console.WriteLine("Notifications enabled");
// Wait for notifications...
await Task.Delay(TimeSpan.FromSeconds(10));
// Disable notifications
await cccd.WriteValueAsync(new byte[] { 0x00, 0x00 });
Console.WriteLine("Notifications disabled");
}
Read Characteristic Description
async Task<string> GetCharacteristicDescriptionAsync(IBluetoothRemoteCharacteristic characteristic)
{
var descriptionUuid = Guid.Parse("00002901-0000-1000-8000-00805F9B34FB");
var descriptor = characteristic.GetDescriptorOrDefault(descriptionUuid);
if (descriptor != null && descriptor.CanRead)
{
var value = await descriptor.ReadValueAsync();
return Encoding.UTF8.GetString(value.ToArray());
}
return characteristic.Name; // Fallback to characteristic name
}
Best Practices
Use High-Level APIs: For notifications, use
StartListeningAsync()instead of manually managing CCCD// Recommended await characteristic.StartListeningAsync(); // Not recommended (unless you have a specific need) var cccd = characteristic.GetDescriptor(cccdUuid); await cccd.WriteValueAsync(new byte[] { 0x01, 0x00 });Explore Descriptors Only When Needed: Use
ExplorationDepth.Descriptorsonly if you actually need descriptor access// Only if you need descriptors await device.ExploreServicesAsync(ServiceExplorationOptions.Full);Check Capabilities: Always check
CanRead/CanWriteif (descriptor.CanRead) await descriptor.ReadValueAsync();Handle Errors Gracefully: Descriptor operations can fail
try { await descriptor.ReadValueAsync(); } catch (DescriptorCantReadException) { Console.WriteLine("Descriptor doesn't support read"); } catch (DeviceNotConnectedException) { Console.WriteLine("Device disconnected"); }Use Timeouts: Prevent operations from hanging
await descriptor.ReadValueAsync( timeout: TimeSpan.FromSeconds(5) );
Troubleshooting
Descriptor Not Found
- Ensure you used
ExplorationDepth.Descriptorswhen exploring - Not all characteristics have descriptors
- Check if the descriptor UUID is correct
Read/Write Fails
- Check
CanRead/CanWritebefore attempting operations - Ensure device is connected
- Some descriptors require authentication/pairing
- Verify correct descriptor UUID
CCCD Issues
- Use the high-level
StartListeningAsync()API instead of manual CCCD control - If manual control is needed, ensure correct byte order (little-endian)
- Check that the characteristic actually supports notifications
Performance
- Descriptors are rarely used in typical applications
- Only explore descriptors if you actually need them
- Reading descriptors adds overhead - cache results if needed
When Do You Need Descriptors?
In most cases, you don't need to work with descriptors directly:
You DON'T need descriptors for:
- Reading characteristic values (use
ReadValueAsync()) - Writing characteristic values (use
WriteValueAsync()) - Receiving notifications (use
StartListeningAsync()- CCCD is handled automatically)
You MIGHT need descriptors for:
- Reading user-friendly characteristic descriptions
- Understanding the data format and units
- Advanced custom protocol implementations
- Debugging and device exploration tools
Related Topics
- Characteristic - The parent of descriptors
- Service - Exploring services and characteristics
- Device - Connection and exploration