Saturday, 2 August 2025

Connecting to BLE Beacons in .NET MAUI

To connect to and interact with BLE beacons in a .NET MAUI application, we'll need to modify our approach since beacons typically broadcast data rather than maintain a full connection. Here's how to implement beacon detection:

Beacon Detection Implementation

1. Update the BleService for Beacon Scanning

csharp
using Plugin.BLE;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.EventArgs;

public class BleService
{
    private readonly IAdapter _adapter;
    private List<IDevice> _discoveredDevices = new();
    
    public event EventHandler<string> StatusUpdated;
    public event EventHandler<BeaconData> BeaconDetected;

    public BleService()
    {
        _adapter = CrossBluetoothLE.Current.Adapter;
        _adapter.ScanMode = ScanMode.LowPower;
        _adapter.DeviceDiscovered += OnDeviceDiscovered;
    }

    public async Task StartBeaconScanningAsync()
    {
        if (!await CheckPermissions())
        {
            StatusUpdated?.Invoke(this, "Permissions not granted");
            return;
        }

        _discoveredDevices.Clear();
        await _adapter.StartScanningForDevicesAsync();
    }

    public async Task StopBeaconScanningAsync()
    {
        await _adapter.StopScanningForDevicesAsync();
    }

    private void OnDeviceDiscovered(object sender, DeviceEventArgs args)
    {
        if (!_discoveredDevices.Contains(args.Device))
        {
            _discoveredDevices.Add(args.Device);
            
            // Check if this is a beacon (has advertisement data)
            if (args.Device.AdvertisementRecords?.Any() ?? false)
            {
                var beaconData = ParseAdvertisementData(args.Device.AdvertisementRecords);
                if (beaconData != null)
                {
                    BeaconDetected?.Invoke(this, beaconData);
                }
            }
        }
    }

    private BeaconData ParseAdvertisementData(IEnumerable<AdvertisementRecord> records)
    {
        foreach (var record in records)
        {
            // Check for iBeacon
            if (record.Type == AdvertisementRecordType.ManufacturerSpecificData)
            {
                var data = record.Data;
                if (data.Length >= 23 && data[0] == 0x4C && data[1] == 0x00)
                {
                    // iBeacon format
                    var uuidBytes = new byte[16];
                    Array.Copy(data, 4, uuidBytes, 0, 16);
                    var uuid = new Guid(uuidBytes);
                    var major = (ushort)((data[20] << 8) + data[21]);
                    var minor = (ushort)((data[22] << 8) + data[23]);
                    var txPower = (sbyte)data[24];
                    
                    return new BeaconData
                    {
                        Type = BeaconType.iBeacon,
                        Uuid = uuid,
                        Major = major,
                        Minor = minor,
                        TxPower = txPower,
                        Rssi = record.Device.Rssi
                    };
                }
            }
            
            // Check for AltBeacon
            if (record.Type == AdvertisementRecordType.ManufacturerSpecificData)
            {
                var data = record.Data;
                if (data.Length >= 25 && data[0] == 0xBE && data[1] == 0xAC)
                {
                    // AltBeacon format
                    var beaconId = new byte[20];
                    Array.Copy(data, 2, beaconId, 0, 20);
                    var manufacturer = (ushort)((data[0] << 8) + data[1]);
                    var txPower = (sbyte)data[24];
                    
                    return new BeaconData
                    {
                        Type = BeaconType.AltBeacon,
                        ManufacturerId = manufacturer,
                        BeaconId = beaconId,
                        TxPower = txPower,
                        Rssi = record.Device.Rssi
                    };
                }
            }
            
            // Check for Eddystone
            if (record.Type == AdvertisementRecordType.ServiceData)
            {
                var data = record.Data;
                if (data.Length > 2 && data[0] == 0xAA && data[1] == 0xFE)
                {
                    // Eddystone frame type
                    var frameType = data[2];
                    
                    if (frameType == 0x00) // UID frame
                    {
                        var namespaceId = new byte[10];
                        Array.Copy(data, 3, namespaceId, 0, 10);
                        var instanceId = new byte[6];
                        Array.Copy(data, 13, instanceId, 0, 6);
                        var txPower = (sbyte)data[19];
                        
                        return new BeaconData
                        {
                            Type = BeaconType.Eddystone,
                            FrameType = EddystoneFrameType.UID,
                            NamespaceId = namespaceId,
                            InstanceId = instanceId,
                            TxPower = txPower,
                            Rssi = record.Device.Rssi
                        };
                    }
                    else if (frameType == 0x10) // URL frame
                    {
                        var txPower = (sbyte)data[3];
                        var url = System.Text.Encoding.UTF8.GetString(data, 4, data.Length - 4);
                        
                        return new BeaconData
                        {
                            Type = BeaconType.Eddystone,
                            FrameType = EddystoneFrameType.URL,
                            Url = url,
                            TxPower = txPower,
                            Rssi = record.Device.Rssi
                        };
                    }
                }
            }
        }
        
        return null;
    }

    private async Task<bool> CheckPermissions()
    {
        var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
        if (status != PermissionStatus.Granted)
        {
            status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
        }
        return status == PermissionStatus.Granted;
    }
}

public class BeaconData
{
    public BeaconType Type { get; set; }
    public int Rssi { get; set; }
    public sbyte TxPower { get; set; }
    
    // iBeacon fields
    public Guid Uuid { get; set; }
    public ushort Major { get; set; }
    public ushort Minor { get; set; }
    
    // AltBeacon fields
    public ushort ManufacturerId { get; set; }
    public byte[] BeaconId { get; set; }
    
    // Eddystone fields
    public EddystoneFrameType? FrameType { get; set; }
    public byte[] NamespaceId { get; set; }
    public byte[] InstanceId { get; set; }
    public string Url { get; set; }
    
    public double? Distance
    {
        get
        {
            if (Rssi == 0) return null;
            
            // Simple distance approximation
            double ratio = Rssi * 1.0 / TxPower;
            if (ratio < 1.0)
            {
                return Math.Pow(ratio, 10);
            }
            else
            {
                return (0.89976) * Math.Pow(ratio, 7.7095) + 0.111;
            }
        }
    }
}

public enum BeaconType
{
    iBeacon,
    AltBeacon,
    Eddystone,
    Unknown
}

public enum EddystoneFrameType
{
    UID = 0x00,
    URL = 0x10,
    TLM = 0x20,
    EID = 0x30
}

2. Update the ViewModel for Beacon Detection

csharp
public class MainViewModel : INotifyPropertyChanged
{
    private readonly BleService _bleService;
    private bool _isScanning;
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    public ObservableCollection<BeaconData> DetectedBeacons { get; } = new();
    
    public ICommand ScanCommand { get; }
    public ICommand StopScanCommand { get; }
    
    public bool IsScanning
    {
        get => _isScanning;
        set
        {
            if (_isScanning != value)
            {
                _isScanning = value;
                OnPropertyChanged();
                OnPropertyChanged(nameof(IsNotScanning));
            }
        }
    }
    
    public bool IsNotScanning => !IsScanning;
    
    public MainViewModel(BleService bleService)
    {
        _bleService = bleService;
        
        ScanCommand = new Command(async () => await ScanBeacons());
        StopScanCommand = new Command(async () => await StopScanning());
        
        _bleService.BeaconDetected += (sender, beacon) =>
        {
            MainThread.BeginInvokeOnMainThread(() =>
            {
                // Update existing beacon or add new one
                var existing = DetectedBeacons.FirstOrDefault(b => 
                    b.Type == beacon.Type && 
                    ((b.Type == BeaconType.iBeacon && b.Uuid == beacon.Uuid && b.Major == beacon.Major && b.Minor == beacon.Minor) ||
                     (b.Type == BeaconType.AltBeacon && b.BeaconId.SequenceEqual(beacon.BeaconId)) ||
                     (b.Type == BeaconType.Eddystone && b.FrameType == beacon.FrameType && 
                      ((beacon.FrameType == EddystoneFrameType.UID && b.NamespaceId.SequenceEqual(beacon.NamespaceId) && b.InstanceId.SequenceEqual(beacon.InstanceId)) ||
                       (beacon.FrameType == EddystoneFrameType.URL && b.Url == beacon.Url)))));
                
                if (existing != null)
                {
                    var index = DetectedBeacons.IndexOf(existing);
                    DetectedBeacons[index] = beacon; // Update with fresh data
                }
                else
                {
                    DetectedBeacons.Add(beacon);
                }
            });
        };
    }
    
    private async Task ScanBeacons()
    {
        DetectedBeacons.Clear();
        IsScanning = true;
        await _bleService.StartBeaconScanningAsync();
    }
    
    private async Task StopScanning()
    {
        IsScanning = false;
        await _bleService.StopBeaconScanningAsync();
    }
    
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

3. Update the MainPage XAML for Beacon Display

xml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="BleBeaconApp.MainPage"
             Title="BLE Beacon Scanner">
    
    <ScrollView>
        <VerticalStackLayout Spacing="20" Padding="20">
            
            <Label Text="BLE Beacon Scanner" 
                   FontSize="Title"
                   HorizontalOptions="Center"/>
            
            <Button Text="Start Beacon Scan" 
                    Command="{Binding ScanCommand}"
                    IsEnabled="{Binding IsNotScanning}"
                    HorizontalOptions="Fill"/>
            
            <Button Text="Stop Scan" 
                    Command="{Binding StopScanCommand}"
                    IsEnabled="{Binding IsScanning}"
                    HorizontalOptions="Fill"/>
            
            <ActivityIndicator IsRunning="{Binding IsScanning}"
                             IsVisible="{Binding IsScanning}"/>
            
            <Label Text="Detected Beacons:" FontAttributes="Bold"/>
            
            <CollectionView ItemsSource="{Binding DetectedBeacons}">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <Frame Padding="10" Margin="0,5">
                            <VerticalStackLayout>
                                <Label Text="{Binding Type}" FontAttributes="Bold"/>
                                
                                <Label Text="{Binding Distance, StringFormat='Approx. distance: {0:F2}m'}"
                                       IsVisible="{Binding Distance.HasValue}"/>
                                
                                <Label Text="{Binding Rssi, StringFormat='RSSI: {0} dBm'}"/>
                                
                                <!-- iBeacon specific -->
                                <StackLayout IsVisible="{Binding Type, Converter={StaticResource BeaconTypeConverter}, ConverterParameter=iBeacon}">
                                    <Label Text="{Binding Uuid, StringFormat='UUID: {0}'}"/>
                                    <Label Text="{Binding Major, StringFormat='Major: {0}'}"/>
                                    <Label Text="{Binding Minor, StringFormat='Minor: {0}'}"/>
                                </StackLayout>
                                
                                <!-- Eddystone specific -->
                                <StackLayout IsVisible="{Binding Type, Converter={StaticResource BeaconTypeConverter}, ConverterParameter=Eddystone}">
                                    <Label Text="{Binding FrameType, StringFormat='Frame type: {0}'}"/>
                                    <Label Text="{Binding Url, StringFormat='URL: {0}'}"
                                           IsVisible="{Binding FrameType, Converter={StaticResource EddystoneFrameTypeConverter}, ConverterParameter=URL}"/>
                                </StackLayout>
                            </VerticalStackLayout>
                        </Frame>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
            
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

4. Add Value Converters

Create a new class BeaconTypeConverter.cs:

csharp
public class BeaconTypeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is BeaconType beaconType && parameter is string param)
        {
            return beaconType.ToString() == param;
        }
        return false;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class EddystoneFrameTypeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is EddystoneFrameType frameType && parameter is string param)
        {
            return frameType.ToString() == param;
        }
        return false;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

5. Register Converters in App.xaml

Add to App.xaml:

xml
<Application.Resources>
    <ResourceDictionary>
        <local:BeaconTypeConverter x:Key="BeaconTypeConverter"/>
        <local:EddystoneFrameTypeConverter x:Key="EddystoneFrameTypeConverter"/>
    </ResourceDictionary>
</Application.Resources>

Key Features of This Implementation

  1. Multi-Beacon Support: Detects iBeacon, AltBeacon, and Eddystone formats

  2. Distance Estimation: Calculates approximate distance based on RSSI and TxPower

  3. Real-time Updates: Refreshes beacon information as new data arrives

  4. Efficient Scanning: Uses low-power scanning mode for beacon detection

How to Use

  1. Start the scan to begin detecting nearby beacons

  2. The app will display:

    • Beacon type (iBeacon, Eddystone, etc.)

    • Beacon-specific identifiers (UUID/major/minor for iBeacon, URL for Eddystone, etc.)

    • Signal strength (RSSI)

    • Approximate distance

  3. Stop scanning when done to conserve battery

Notes

  1. Beacon detection works without establishing a connection - beacons broadcast their information periodically.

  2. The distance calculation is approximate and can be affected by many environmental factors.

  3. For production use, you might want to:

    • Add filtering for specific beacon IDs

    • Implement region monitoring (enter/exit events)

    • Add background scanning capability

    • Implement more sophisticated distance calculations

  4. Remember that beacon scanning requires location permissions on both Android and iOS.

No comments:

Post a Comment

Complete Guide: Building a Live Cricket Streaming App for 100M Users

Comprehensive guide to building a scalable live cricket streaming platform for 100M users, covering backend infrastructure, streaming techno...