Saturday, 2 August 2025

NET MAUI application that connects to a Bluetooth Low Energy (BLE) device

Here's a comprehensive guide to building a .NET MAUI application that connects to a Bluetooth Low Energy (BLE) device, pairs with it, and triggers a ring function.

Prerequisites

  1. Visual Studio 2022 with .NET MAUI workload installed

  2. A BLE device that supports the "Find Me" profile (or custom service with ring functionality)

  3. Basic knowledge of XAML and C#

Step 1: Set up the .NET MAUI Project

  1. Create a new .NET MAUI App project in Visual Studio

  2. Add required permissions and platform-specific configurations

Step 2: Add Required NuGet Packages

Add these NuGet packages to your project:

  • Plugin.BLE (Cross-platform BLE plugin)

  • Permissions (For handling runtime permissions)

Step 3: Implement the BLE Service

Create a new class BleService.cs:

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

public class BleService
{
    private IBluetoothLE _ble;
    private IAdapter _adapter;
    private IDevice _connectedDevice;
    private IService _findMeService;
    private ICharacteristic _alertCharacteristic;
    
    public event EventHandler<string> StatusUpdated;
    public event EventHandler<string> DeviceFound;
    public event EventHandler<bool> ConnectionStatusChanged;

    public BleService()
    {
        _ble = CrossBluetoothLE.Current;
        _adapter = CrossBluetoothLE.Current.Adapter;
        
        _adapter.DeviceDiscovered += OnDeviceDiscovered;
        _adapter.DeviceConnected += OnDeviceConnected;
        _adapter.DeviceDisconnected += OnDeviceDisconnected;
    }

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

        if (!_ble.IsAvailable)
        {
            StatusUpdated?.Invoke(this, "Bluetooth not available");
            return;
        }

        if (_ble.State != BluetoothState.On)
        {
            StatusUpdated?.Invoke(this, "Bluetooth is off");
            return;
        }

        _adapter.ScanMode = ScanMode.LowLatency;
        await _adapter.StartScanningForDevicesAsync();
    }

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

    public async Task ConnectToDeviceAsync(IDevice device)
    {
        try
        {
            StatusUpdated?.Invoke(this, $"Connecting to {device.Name}...");
            
            var connectParameters = new ConnectParameters(
                autoConnect: false, 
                forceBleTransport: true);
            
            await _adapter.ConnectToDeviceAsync(device, connectParameters);
            
            // Find the "Find Me" service (standard UUID)
            _findMeService = await device.GetServiceAsync(Guid.Parse("00001802-0000-1000-8000-00805f9b34fb"));
            
            if (_findMeService != null)
            {
                // Get the alert level characteristic
                _alertCharacteristic = await _findMeService.GetCharacteristicAsync(
                    Guid.Parse("00002a06-0000-1000-8000-00805f9b34fb"));
                
                StatusUpdated?.Invoke(this, "Connected and services discovered");
            }
            else
            {
                StatusUpdated?.Invoke(this, "Find Me service not found");
            }
        }
        catch (Exception ex)
        {
            StatusUpdated?.Invoke(this, $"Connection failed: {ex.Message}");
        }
    }

    public async Task RingDeviceAsync()
    {
        if (_alertCharacteristic != null)
        {
            try
            {
                // High alert level (value 2) - typically makes the device ring/vibrate
                await _alertCharacteristic.WriteAsync(new byte[] { 2 });
                StatusUpdated?.Invoke(this, "Ring command sent");
            }
            catch (Exception ex)
            {
                StatusUpdated?.Invoke(this, $"Failed to ring device: {ex.Message}");
            }
        }
        else
        {
            StatusUpdated?.Invoke(this, "No alert characteristic available");
        }
    }

    public async Task DisconnectDeviceAsync()
    {
        if (_connectedDevice != null)
        {
            await _adapter.DisconnectDeviceAsync(_connectedDevice);
        }
    }

    private async Task<bool> CheckPermissions()
    {
        var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
        if (status != PermissionStatus.Granted)
        {
            status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
        }
        
        // On Android, Bluetooth requires location permission
        return status == PermissionStatus.Granted;
    }

    private void OnDeviceDiscovered(object sender, DeviceEventArgs args)
    {
        DeviceFound?.Invoke(this, $"{args.Device.Name ?? "Unnamed"} ({args.Device.Id})");
    }

    private void OnDeviceConnected(object sender, DeviceEventArgs args)
    {
        _connectedDevice = args.Device;
        ConnectionStatusChanged?.Invoke(this, true);
        StatusUpdated?.Invoke(this, $"Connected to {args.Device.Name}");
    }

    private void OnDeviceDisconnected(object sender, DeviceEventArgs args)
    {
        _connectedDevice = null;
        ConnectionStatusChanged?.Invoke(this, false);
        StatusUpdated?.Invoke(this, "Device disconnected");
    }
}

Step 4: Create the Main Page UI

Update MainPage.xaml:

xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="BleRingApp.MainPage"
             Title="BLE Device Ringer">
    
    <ScrollView>
        <VerticalStackLayout Spacing="20" Padding="20">
            
            <Label Text="BLE Device Ringer" 
                   FontSize="Title"
                   HorizontalOptions="Center"/>
            
            <Button Text="Scan for Devices" 
                    Command="{Binding ScanCommand}"
                    IsEnabled="{Binding IsNotScanning}"
                    HorizontalOptions="Fill"/>
            
            <Button Text="Stop Scanning" 
                    Command="{Binding StopScanCommand}"
                    IsEnabled="{Binding IsScanning}"
                    HorizontalOptions="Fill"/>
            
            <ActivityIndicator IsRunning="{Binding IsScanning}"
                               IsVisible="{Binding IsScanning}"/>
            
            <Label Text="Discovered Devices:" FontAttributes="Bold"/>
            
            <CollectionView ItemsSource="{Binding DiscoveredDevices}"
                           SelectionMode="Single"
                           SelectedItem="{Binding SelectedDevice}">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <Label Text="{Binding .}" Padding="10"/>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
            
            <Button Text="Connect to Selected Device" 
                    Command="{Binding ConnectCommand}"
                    IsEnabled="{Binding HasSelectedDevice}"
                    HorizontalOptions="Fill"/>
            
            <Button Text="Ring Device" 
                    Command="{Binding RingCommand}"
                    IsEnabled="{Binding IsConnected}"
                    HorizontalOptions="Fill"/>
            
            <Button Text="Disconnect" 
                    Command="{Binding DisconnectCommand}"
                    IsEnabled="{Binding IsConnected}"
                    HorizontalOptions="Fill"/>
            
            <Label Text="Status:" FontAttributes="Bold"/>
            <Label Text="{Binding StatusMessage}"
                   FontAttributes="Italic"/>
            
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

Step 5: Create the ViewModel

Create a new class MainViewModel.cs:

csharp
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

public class MainViewModel : INotifyPropertyChanged
{
    private readonly BleService _bleService;
    
    private string _statusMessage = "Ready";
    private bool _isScanning;
    private bool _isConnected;
    private string _selectedDevice;
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    public ObservableCollection<string> DiscoveredDevices { get; } = new();
    
    public ICommand ScanCommand { get; }
    public ICommand StopScanCommand { get; }
    public ICommand ConnectCommand { get; }
    public ICommand RingCommand { get; }
    public ICommand DisconnectCommand { get; }
    
    public string StatusMessage
    {
        get => _statusMessage;
        set => SetField(ref _statusMessage, value);
    }
    
    public bool IsScanning
    {
        get => _isScanning;
        set
        {
            SetField(ref _isScanning, value);
            OnPropertyChanged(nameof(IsNotScanning));
        }
    }
    
    public bool IsNotScanning => !IsScanning;
    
    public bool IsConnected
    {
        get => _isConnected;
        set => SetField(ref _isConnected, value);
    }
    
    public string SelectedDevice
    {
        get => _selectedDevice;
        set
        {
            SetField(ref _selectedDevice, value);
            OnPropertyChanged(nameof(HasSelectedDevice));
        }
    }
    
    public bool HasSelectedDevice => !string.IsNullOrEmpty(SelectedDevice);
    
    public MainViewModel(BleService bleService)
    {
        _bleService = bleService;
        
        ScanCommand = new Command(async () => await ScanDevices());
        StopScanCommand = new Command(async () => await StopScanning());
        ConnectCommand = new Command(async () => await ConnectToDevice());
        RingCommand = new Command(async () => await RingDevice());
        DisconnectCommand = new Command(async () => await DisconnectDevice());
        
        _bleService.StatusUpdated += (sender, message) => 
            StatusMessage = message;
        
        _bleService.DeviceFound += (sender, device) =>
        {
            if (!DiscoveredDevices.Contains(device))
            {
                MainThread.BeginInvokeOnMainThread(() => DiscoveredDevices.Add(device));
            }
        };
        
        _bleService.ConnectionStatusChanged += (sender, connected) =>
            IsConnected = connected;
    }
    
    private async Task ScanDevices()
    {
        DiscoveredDevices.Clear();
        IsScanning = true;
        await _bleService.StartScanningAsync();
    }
    
    private async Task StopScanning()
    {
        IsScanning = false;
        await _bleService.StopScanningAsync();
    }
    
    private async Task ConnectToDevice()
    {
        if (SelectedDevice != null)
        {
            // Extract the device ID from the displayed string
            var deviceId = SelectedDevice.Split('(')[1].TrimEnd(')');
            var devices = _bleService.GetDiscoveredDevices();
            var device = devices.FirstOrDefault(d => d.Id.ToString() == deviceId);
            
            if (device != null)
            {
                await _bleService.ConnectToDeviceAsync(device);
            }
        }
    }
    
    private async Task RingDevice()
    {
        await _bleService.RingDeviceAsync();
    }
    
    private async Task DisconnectDevice()
    {
        await _bleService.DisconnectDeviceAsync();
    }
    
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    
    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

Step 6: Update the MainPage Code-behind

Update MainPage.xaml.cs:

csharp
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        var bleService = new BleService();
        BindingContext = new MainViewModel(bleService);
    }
}

Step 7: Add Platform-Specific Configuration

Android

  1. Add these permissions to AndroidManifest.xml:

xml
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
  1. Add this to the <application> tag:

xml
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

iOS

Add these entries to Info.plist:

xml
<key>NSBluetoothAlwaysUsageDescription</key>
<string>We need Bluetooth access to connect to your device</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>We need Bluetooth access to connect to your device</string>
<key>UIBackgroundModes</key>
<array>
    <string>bluetooth-central</string>
    <string>bluetooth-peripheral</string>
</array>

Step 8: Testing the App

  1. Deploy the app to your device (Bluetooth testing doesn't work well on emulators)

  2. Make sure your BLE device is in pairing mode

  3. Follow the app flow:

    • Scan for devices

    • Select your device from the list

    • Connect to the device

    • Use the "Ring Device" button to trigger the alert

Notes

  1. This implementation uses the standard "Find Me" profile (0x1802) with the Alert Level characteristic (0x2A06). If your device uses custom services, you'll need to modify the UUIDs in the BleService class.

  2. The actual "ring" functionality depends on your BLE device's implementation. Some devices might require different values or a different characteristic entirely.

  3. For more complex BLE interactions, you might need to implement service/characteristic discovery more thoroughly.

  4. Error handling is basic in this example - you may want to enhance it for production use.

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...