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
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
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
<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
:
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
:
<Application.Resources> <ResourceDictionary> <local:BeaconTypeConverter x:Key="BeaconTypeConverter"/> <local:EddystoneFrameTypeConverter x:Key="EddystoneFrameTypeConverter"/> </ResourceDictionary> </Application.Resources>
Key Features of This Implementation
Multi-Beacon Support: Detects iBeacon, AltBeacon, and Eddystone formats
Distance Estimation: Calculates approximate distance based on RSSI and TxPower
Real-time Updates: Refreshes beacon information as new data arrives
Efficient Scanning: Uses low-power scanning mode for beacon detection
How to Use
Start the scan to begin detecting nearby beacons
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
Stop scanning when done to conserve battery
Notes
Beacon detection works without establishing a connection - beacons broadcast their information periodically.
The distance calculation is approximate and can be affected by many environmental factors.
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
Remember that beacon scanning requires location permissions on both Android and iOS.
No comments:
Post a Comment