From a052b05535d0b037a6e1e8fb907c1d1f5db30897 Mon Sep 17 00:00:00 2001 From: Paul-Winpenny <92634321+Paul-Winpenny@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:07:39 +0100 Subject: [PATCH] Bluetooth works! (BLE) --- .../ViewModels/ConnectionPageViewModel.cs | 237 +++++++++++++++--- App/RobobinApp/Views/ConnectionPage.xaml | 36 ++- 2 files changed, 240 insertions(+), 33 deletions(-) diff --git a/App/RobobinApp/ViewModels/ConnectionPageViewModel.cs b/App/RobobinApp/ViewModels/ConnectionPageViewModel.cs index 3bea0065..8c08c32b 100644 --- a/App/RobobinApp/ViewModels/ConnectionPageViewModel.cs +++ b/App/RobobinApp/ViewModels/ConnectionPageViewModel.cs @@ -1,5 +1,5 @@ using System.Collections.ObjectModel; -using System.Diagnostics; // Add this for Debug.WriteLine +using System.Diagnostics; using System.Windows.Input; using Microsoft.Maui.Controls; using RobobinApp.Views; @@ -7,38 +7,125 @@ using Plugin.BLE; using Plugin.BLE.Abstractions.Contracts; using Plugin.BLE.Abstractions.Exceptions; using Plugin.BLE.Abstractions.EventArgs; - +using System.Threading.Tasks; +using Plugin.BLE.Abstractions; namespace RobobinApp.ViewModels { public class ConnectionPageViewModel : BaseViewModel { private BluetoothDevice _selectedDevice; + private IDevice _connectedDevice; // Track the connected device public ICommand GoHomeCommand { get; } public ObservableCollection<BluetoothDevice> BluetoothDevices { get; } public ICommand ConnectCommand { get; } + public ICommand SendWifiInfoCommand { get; } + public ICommand DisconnectCommand { get; } // Command for disconnecting + private bool _isConnected; // Track connection state + private string ssid; + private string password; + public bool IsConnected + { + get => _isConnected; + set + { + _isConnected = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ConnectButtonText)); // Update button text when connection state changes + } + } + + public string SSID + { + get => ssid; + set + { + ssid = value; + OnPropertyChanged(nameof(SSID)); + } + } + + public string Password + { + get => password; + set + { + password = value; + OnPropertyChanged(nameof(Password)); + } + } - // BLE plugin fields private readonly IAdapter _adapter; private readonly IBluetoothLE _bluetoothLE; + public string ConnectButtonText => IsConnected ? "Disconnect" : "Connect"; public ConnectionPageViewModel() { BluetoothDevices = new ObservableCollection<BluetoothDevice>(); ConnectCommand = new Command(OnConnect); + DisconnectCommand = new Command(OnDisconnect); // Initialize disconnect command + SendWifiInfoCommand = new Command(OnSendWifiInfo); GoHomeCommand = new Command(async () => await OnGoHome()); - - // Initialize BLE manager and adapter + ConnectCommand = new Command(OnToggleConnection); _bluetoothLE = CrossBluetoothLE.Current; _adapter = _bluetoothLE.Adapter; - - // Subscribe to the DeviceDiscovered event _adapter.DeviceDiscovered += OnDeviceDiscovered; - // Check and request Bluetooth permissions before scanning Debug.WriteLine("Checking and requesting Bluetooth permissions."); CheckAndRequestBluetoothPermissions(); } + private async void OnSendWifiInfo(object obj) + { + if (!_isConnected || _connectedDevice == null) + { + Debug.WriteLine("Cannot send WiFi information: No device is connected."); + await Application.Current.MainPage.DisplayAlert("Error", "No device is connected.", "OK"); + return; + } + + try + { + Debug.WriteLine($"Sending WiFi information to {_connectedDevice.Name}..."); + + // Retrieve the UART service + var services = await _connectedDevice.GetServicesAsync(); + var uartService = services.FirstOrDefault(s => s.Id == Guid.Parse("6e400001-b5a3-f393-e0a9-e50e24dcca9e")); + + if (uartService == null) + { + Debug.WriteLine("UART service not found."); + await Application.Current.MainPage.DisplayAlert("Error", "UART service not found on the device.", "OK"); + return; + } + + // Get the RX characteristic for writing data + var characteristics = await uartService.GetCharacteristicsAsync(); + var rxCharacteristic = characteristics.FirstOrDefault(c => c.Id == Guid.Parse("6e400002-b5a3-f393-e0a9-e50e24dcca9e")); + + if (rxCharacteristic == null) + { + Debug.WriteLine("RX characteristic not found."); + await Application.Current.MainPage.DisplayAlert("Error", "RX characteristic not found on the device.", "OK"); + return; + } + + // Prepare the data to send + string wifiInfo = $"{SSID},{Password}"; // Format as SSID,Password + byte[] data = System.Text.Encoding.UTF8.GetBytes(wifiInfo); + + // Write the data to the characteristic + await rxCharacteristic.WriteAsync(data); + + Debug.WriteLine("WiFi information sent successfully."); + await Application.Current.MainPage.DisplayAlert("Success", "WiFi information sent successfully.", "OK"); + } + catch (Exception ex) + { + Debug.WriteLine($"Error sending WiFi information: {ex.Message}"); + await Application.Current.MainPage.DisplayAlert("Error", $"Failed to send WiFi information: {ex.Message}", "OK"); + } + } + public BluetoothDevice SelectedDevice { @@ -49,28 +136,34 @@ namespace RobobinApp.ViewModels OnPropertyChanged(); } } - + private async void OnToggleConnection() + { + if (IsConnected) + { + OnDisconnect(); // Call the existing disconnect method + } + else + { + OnConnect(); // Call the existing connect method + } + } private async void CheckAndRequestBluetoothPermissions() { - // Check if the Bluetooth scan permission is granted Debug.WriteLine("Checking Bluetooth permissions."); var status = await Permissions.CheckStatusAsync<Permissions.Bluetooth>(); if (status != PermissionStatus.Granted) { Debug.WriteLine("Bluetooth permission not granted, requesting permission."); - // Request permission status = await Permissions.RequestAsync<Permissions.Bluetooth>(); } if (status == PermissionStatus.Granted) { Debug.WriteLine("Bluetooth permission granted, proceeding to scan devices."); - // Permission granted, proceed with scanning devices ScanDevices(); } else { - // Permission denied, handle accordingly Debug.WriteLine("Bluetooth permission denied."); await Application.Current.MainPage.DisplayAlert("Permissions", "Bluetooth scan permission is required to discover devices.", "OK"); } @@ -84,7 +177,6 @@ namespace RobobinApp.ViewModels private async void ScanDevices() { - // Clear the current list of devices before scanning BluetoothDevices.Clear(); Debug.WriteLine("Cleared existing Bluetooth devices. Starting scan..."); @@ -104,7 +196,6 @@ namespace RobobinApp.ViewModels return; } - // Start scanning for devices Debug.WriteLine("Starting scanning for devices..."); await _adapter.StartScanningForDevicesAsync(); } @@ -115,11 +206,16 @@ namespace RobobinApp.ViewModels } } - // New method to handle discovered devices private void OnDeviceDiscovered(object sender, DeviceEventArgs e) { Debug.WriteLine($"Discovered device: ID={e.Device.Id}, Name={e.Device.Name}, State={e.Device.State}, RSSI={e.Device.Rssi}"); + // Filter out devices that are not in a ready state + //if (e.Device.State != DeviceState.Connected && e.Device.State != DeviceState.Disconnected) + //{ + // Debug.WriteLine($"Device {e.Device.Name} is not in a fully ready state (State: {e.Device.State}). Skipping."); + // return; + //} var bluetoothDevice = new BluetoothDevice { @@ -137,8 +233,29 @@ namespace RobobinApp.ViewModels } } - - + private async Task<IDevice> TryConnectAsync(Guid deviceId, int retryCount = 3) + { + IDevice device = null; + for (int attempt = 1; attempt <= retryCount; attempt++) + { + try + { + Debug.WriteLine($"Attempt {attempt}: Connecting to device {deviceId}"); + var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // Create a cancellation token with a timeout + device = await _adapter.ConnectToKnownDeviceAsync(deviceId, new ConnectParameters(false, true), cancellationTokenSource.Token); // Pass the cancellation token + return device; + } + catch (Exception ex) + { + Debug.WriteLine($"Attempt {attempt} failed: {ex.Message}"); + if (attempt == retryCount) + { + throw; // Rethrow the exception if we've exhausted the retries + } + } + } + return device; + } private async void OnConnect() { @@ -148,35 +265,97 @@ namespace RobobinApp.ViewModels return; } - // Logic to connect to the selected Bluetooth device - Debug.WriteLine($"Attempting to connect to {SelectedDevice.Name}..."); + Debug.WriteLine($"Attempting to connect to {SelectedDevice.Name}, MAC Address {SelectedDevice.MacAddress}"); await Application.Current.MainPage.DisplayAlert("Connection", $"Connecting to {SelectedDevice.Name}...", "OK"); try { - // Stop scanning before attempting connection - await _adapter.StopScanningForDevicesAsync(); + await _adapter.StopScanningForDevicesAsync(); // Ensure scanning is stopped. Debug.WriteLine("Stopped scanning for devices."); + IsConnected = true; + _connectedDevice = await TryConnectAsync(Guid.Parse(SelectedDevice.MacAddress)); // Use retry logic for connection - // Find the device using the MAC address - var device = await _adapter.ConnectToKnownDeviceAsync(Guid.Parse(SelectedDevice.MacAddress)); - - // Display successful connection Debug.WriteLine($"Successfully connected to {SelectedDevice.Name}."); - await Application.Current.MainPage.DisplayAlert("Connected", $"Connected to {SelectedDevice.Name}.", "OK"); + var services = await _connectedDevice.GetServicesAsync(); + + foreach (var service in services) + { + Debug.WriteLine($"Service: ID={service.Id}, Name={service.Name}"); + + // Check if the service is the UART service + if (service.Id == Guid.Parse("6e400001-b5a3-f393-e0a9-e50e24dcca9e")) + { + Debug.WriteLine("Found UART service!"); + + // Get the characteristics of the UART service + var characteristics = await service.GetCharacteristicsAsync(); + foreach (var characteristic in characteristics) + { + Debug.WriteLine($"Characteristic: ID={characteristic.Id}, Properties={characteristic.Properties}"); + + if (characteristic.Id == Guid.Parse("6e400002-b5a3-f393-e0a9-e50e24dcca9e")) + { + Debug.WriteLine("Found RX characteristic (for writing data)"); + // Convert the string to a byte array + byte[] data = System.Text.Encoding.UTF8.GetBytes("Hello World"); + // Write to the characteristic + await characteristic.WriteAsync(data); + } + else if (characteristic.Id == Guid.Parse("6e400003-b5a3-f393-e0a9-e50e24dcca9e")) + { + Debug.WriteLine("Found TX characteristic (for notifications)"); + } + } + } + } } catch (DeviceConnectionException ex) { - // Handle connection failure Debug.WriteLine($"Connection error: {ex.Message}"); await Application.Current.MainPage.DisplayAlert("Connection Error", $"Failed to connect: {ex.Message}", "OK"); } + catch (TaskCanceledException ex) + { + Debug.WriteLine($"TaskCanceledException: {ex.Message}"); + await Application.Current.MainPage.DisplayAlert("Connection Error", "Connection timed out.", "OK"); + _connectedDevice = null; + IsConnected = false; + } catch (Exception ex) { Debug.WriteLine($"General connection error: {ex.Message}"); await Application.Current.MainPage.DisplayAlert("Connection Error", $"An error occurred: {ex.Message}", "OK"); } } + + + private async void OnDisconnect() + { + if (_connectedDevice != null) + { + try + { + Debug.WriteLine($"Attempting to disconnect from {_connectedDevice.Name}"); + await _adapter.DisconnectDeviceAsync(_connectedDevice); + Debug.WriteLine("Disconnected successfully."); + + // Dispose of the device reference + _connectedDevice.Dispose(); + _connectedDevice = null; + IsConnected = false; + await Application.Current.MainPage.DisplayAlert("Disconnected", "Device disconnected successfully.", "OK"); + } + catch (Exception ex) + { + Debug.WriteLine($"Error during disconnection: {ex.Message}"); + await Application.Current.MainPage.DisplayAlert("Disconnection Error", $"Failed to disconnect: {ex.Message}", "OK"); + } + } + else + { + Debug.WriteLine("No device to disconnect."); + } + } } public class BluetoothDevice @@ -184,4 +363,4 @@ namespace RobobinApp.ViewModels public string Name { get; set; } public string MacAddress { get; set; } } -} \ No newline at end of file +} diff --git a/App/RobobinApp/Views/ConnectionPage.xaml b/App/RobobinApp/Views/ConnectionPage.xaml index cf108b67..7d7ae169 100644 --- a/App/RobobinApp/Views/ConnectionPage.xaml +++ b/App/RobobinApp/Views/ConnectionPage.xaml @@ -27,12 +27,40 @@ </ListView.ItemTemplate> </ListView> - <!-- Connect Button --> - <Button Text="Connect" - Command="{Binding ConnectCommand}" + <Button Text="{Binding ConnectButtonText}" + Command="{Binding ConnectCommand}" IsEnabled="{Binding SelectedDevice, Converter={StaticResource NullToBooleanConverter}}" HorizontalOptions="Center" VerticalOptions="End" /> + + <!-- Section for SSID and Password - Initially Hidden --> + <StackLayout IsVisible="{Binding IsConnected}" + Padding="10" + BackgroundColor="LightGray" + HorizontalOptions="FillAndExpand" + VerticalOptions="Start"> + <Label Text="SSID:" + FontSize="Medium" + HorizontalOptions="Start" /> + <Entry x:Name="SSIDEntry" + Placeholder="Enter SSID" + Text="{Binding SSID}" + HorizontalOptions="Fill" /> + + <Label Text="Password:" + FontSize="Medium" + HorizontalOptions="Start" /> + <Entry x:Name="PasswordEntry" + Placeholder="Enter Password" + Text="{Binding Password}" + IsPassword="True" + HorizontalOptions="Fill" /> + + <Button Text="Send WiFi Information" + Command="{Binding SendWifiInfoCommand}" + HorizontalOptions="Center" + VerticalOptions="End" /> + </StackLayout> + </StackLayout> - </ContentPage> -- GitLab