Profile Migration
This document explains how to move from the archived characteristic access model to the current service-definition-based architecture.
📚 For comprehensive documentation of the current service definitions system, see Service Definitions and Profiles.
Why This Changed
The archived implementation centered around:
CharacteristicAccessService<T>and related factory helpers- reflection-based
[ServiceDefinition]discovery - UUID-only characteristic lookup
- mutable service metadata assigned after construction
The current implementation replaces that with:
- immutable service definitions
- pair-keyed characteristic registration by
(serviceId, characteristicId) - explicit DI registration through
BluetoothServiceDefinitionRegistrarDelegate - typed access through
IBluetoothCharacteristicAccessor<TRead, TWrite> - typed listen orchestration through
IBluetoothCharacteristicAccessorListener<TRead> - explicit codecs through
ICharacteristicCodec<TRead, TWrite>
The new model removes ambiguity, is easier to reason about under AOT, and aligns better with the current IBluetoothRemote* abstraction surface.
Concept Mapping
| Archived model | Current model |
|---|---|
CharacteristicAccessService<T> |
IBluetoothCharacteristicAccessor<TRead, TWrite> |
CharacteristicAccessServiceFactory |
CharacteristicCodecFactory + CharacteristicAccessor<TRead, TWrite> |
[ServiceDefinition] discovery |
BluetoothServiceDefinitionRegistrarDelegate in DI |
CharacteristicAccessServicesRepository |
IBluetoothServiceDefinitionRegistry |
| UUID-only characteristic name lookup | (serviceId, characteristicId) lookup |
OnNotificationReceived callback registration |
IBluetoothCharacteristicAccessorListener<TRead> |
Registering Profiles
Archived code often depended on automatic discovery. The new model is explicit.
For the built-in SIG service definitions:
builder.Services.AddBluetoothSigProfiles();
builder.Services.AddBluetoothCoreScanningServices();
If you use AddBluetoothServices() from Bluetooth.Maui, SIG service definitions are already included before the core scanning registrations are finalized.
For custom service definitions, register the definition type through BluetoothServiceDefinitionRegistrar:
builder.Services.AddSingleton<BluetoothServiceDefinitionRegistration>(_ => registry =>
{
BluetoothServiceDefinitionRegistrar.Register(registry, typeof(MyServiceDefinition));
});
Defining Typed Accessors
Archived code often looked like this:
public readonly static CharacteristicAccessService<byte> BatteryLevel =
CharacteristicAccessServiceFactory.CreateForByte(batteryLevelId, "Battery Level");
The current equivalent can stay concise while keeping direct static access:
[BluetoothServiceDefinition]
public static class BatteryServiceDefinition
{
public static readonly string Name = "Battery Service";
public static readonly Guid Id = BluetoothServiceDefinitions.FromShortId(0x180F);
public static readonly IBluetoothCharacteristicAccessor<byte, byte> BatteryLevel =
BluetoothServiceDefinitions.ByteCharacteristic(Id, 0x2A19, Name, "Battery Level");
}
This separates:
- service definition registration and naming
- typed value conversion
- runtime characteristic resolution
while preserving direct static access such as await BatteryServiceDefinition.BatteryLevel.ReadAsync(device)
Reading And Writing
Archived usage:
var level = await BatteryServiceDefinition.BatteryLevel.ReadAsync(device, false);
Current usage remains intuitive:
var level = await BatteryServiceDefinition.BatteryLevel.ReadAsync(device, false);
Writes follow the same pattern:
await myAccessor.WriteAsync(device, value);
Listening For Notifications
The archived callback pattern has been replaced with a listener orchestrator that owns subscription lifecycle.
var listener = new CharacteristicAccessorListener<byte>(
serviceId,
characteristicId,
CharacteristicCodecFactory.ForByte(),
"Battery Service",
"Battery Level");
await listener.SubscribeAsync(device, value =>
{
Console.WriteLine($"Battery: {value}");
});
Important differences:
- listeners are fan-out callbacks, not bool-return handlers
- notification payloads are decoded from event args, not characteristic cache state
- start/stop listening is tied to first/last subscriber
Name Resolution
Archived code resolved characteristic names by UUID only, which breaks when the same characteristic UUID appears in multiple services.
The current implementation resolves names with both service and characteristic UUIDs first, then falls back to UUID-only lookup only when that is unambiguous.
This means custom profiles should always register both:
BluetoothServiceDefinitionBluetoothCharacteristicDefinitionwith the owning service ID
Device Convenience APIs
The current codebase keeps device-level convenience methods, but they now route through service definition accessors instead of the archived repository:
ReadBatteryLevelAsync()ReadFirmwareVersionAsync()ReadSoftwareVersionAsync()ReadHardwareVersionAsync()ReadVersionsAsync()
If you had custom convenience APIs in subclasses, prefer implementing them in terms of service definition accessors rather than direct characteristic traversal.
What Did Not Carry Forward
These archived behaviors were intentionally not preserved:
- reflection-based service definition discovery by default
- repository fallback objects like
UnknownCharacteristicAccessService - silent capability probing that swallowed exceptions
- mutable service metadata assignment after accessor construction
If you still need archive-style object conversion, build a codec adapter around ICharacteristicCodec<TRead, TWrite> instead of reintroducing the old repository model.
Recommended Migration Steps
- Replace each archived static service definition with a service definition class that exposes typed accessors.
- Register service and characteristic names through
BluetoothServiceDefinitionRegistrarDelegate. - Replace
CharacteristicAccessServiceFactory.*calls withCharacteristicCodecFactory.*plusCharacteristicAccessor<TRead, TWrite>. - Replace notification callback registration with
CharacteristicAccessorListener<TRead>. - Remove any reliance on reflection-based discovery or UUID-only characteristic identity.