Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/taublast/-appomobi.maui.ble.connector
https://github.com/taublast/-appomobi.maui.ble.connector
Last synced: 1 day ago
JSON representation
- Host: GitHub
- URL: https://github.com/taublast/-appomobi.maui.ble.connector
- Owner: taublast
- Created: 2023-03-01T10:17:00.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-02-23T17:17:06.000Z (9 months ago)
- Last Synced: 2024-02-23T18:28:52.491Z (9 months ago)
- Language: C#
- Size: 61.5 KB
- Stars: 0
- Watchers: 1
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# AppoMobi.Maui.BLE.Connector
Wrapper for AppoMobi.Maui.BLE (https://github.com/taublast/-AppoMobi.Maui.BLE)
Tested to work fine on Android, iOS, MacCatalyst and Windows.
Remains in -PRE state
## Roadmap:
- Add example app
## Quick Start
The following will add a DI for `IBluetoothLE````csharp
builder
.UseBlootoothLE()
```Create your simple connector by subclassing the provided connector:
```csharp
[Preserve(AllMembers = true)]
public class MyConnector : BLEConnector
{public MyConnector(IBluetoothLE ble) : base(ble)
{
}///
/// Let's say you want to connect to your brand device that has this service id.
///
public static Guid MyKnownDeviceService { get; } = Guid.Parse("0000ffe0-0000-1000-8000-00805f9b34fb");///
/// For your brand device
///
public static string ServiceId { get; set; } = "0000ffe0-0000-1000-8000-00805f9b34fb";///
/// For your brand device
///
public static string CharacteristicId { get; set; } = "0000ffe1-0000-1000-8000-00805f9b34fb";#region EXAMPLE WRITE
public readonly string CommandReadDeviceSettings = "CONF_READ";
public async Task RequestMyDeviceSettings()
{
if (WriteTo != null)
{
byte[] bytes = Encoding.ASCII.GetBytes(CommandReadDeviceSettings);
await WriteTo.Info.WriteAsync(bytes);return true;
}return false;
}#endregion
public event EventHandler DeviceConnectionChanged;
protected override async void OnConnectionChanged()
{base.OnConnectionChanged();
Debug.WriteLine($"DEVICE connected: {IsConnected}");
if (!IsConnected)
{
StopMonitoring(_subscribed);
WriteTo = null;
ConnectedDevice = null;DeviceConnectionChanged?.Invoke(this, false);
}}
///
/// Uses Serial property
///
///
///
public async Task ConnectToSerialDevice(bool needThrow = false)
{#if ANDROID || IOS || MACCATALYST || WINDOWS
//connect
try
{
ConnectionState = ConnectionState.Connecting;StopScanning();
var device = await ConnectDevice(Serial);
if (device == null)
{
throw new Exception($"{Serial} not found");
}LastName = device.Device.Name;
LastSerial = device.Id;
ConnectionState = ConnectionState.Connected;var service = device.Services.FirstOrDefault(x => x.Id.Equals(ServiceId, StringComparison.InvariantCultureIgnoreCase));
if (service == null)
{
//service not found
throw new Exception($"Service {ServiceId} not found");
}var read = service.Characteristics.FirstOrDefault(x => x.Id.Equals(CharacteristicId, StringComparison.InvariantCultureIgnoreCase));
if (read == null)
{
//characteristic not found
throw new Exception($"Characteristic {CharacteristicId} not found");
}//Our brand device uses same characteristic for read/write
if (read.Info.CanWrite)
WriteTo = read; //the actual case
else
{
var write = service.Characteristics.Where(c => c.Info.CanWrite).ToArray();
foreach (var writable in write)
{
Trace.WriteLine($"[W] {writable.Id} {writable.Info.Id} {writable.Info.Name} {writable.Info.Uuid}");
}
//just in case..
WriteTo = write.FirstOrDefault();
}if (ConnectedDevice == null)
throw new Exception("unknown error");await StartMonitoring(read, true);
DeviceConnectionChanged?.Invoke(this, true);
}
catch (Exception e)
{
WriteTo = null;
ConnectedDevice = null;Console.WriteLine(e);
ConnectionState = ConnectionState.Error;
DeviceConnectionChanged?.Invoke(this, false);
if (needThrow)
{
throw e;
}
}
#elsethrow new NotImplementedException();
#endif
}
BleCharacteristicViewModel WriteTo
{
get => _writeTo;
set => _writeTo = value;
}#region PERMISSIONS
///
/// Initialize SDK, parameters are for displaying permissions prompts
///
/// your maui app mainPage to attach permission propts to
/// the title that will be displayed for permission prompts
public void Init(Page mainPage, string appTitile)
{
AppTitle = appTitile;
MainPage = mainPage;
Initialized = true;
}public virtual bool CheckGpsIsAvailable()
{
return BluetoothPermissions.CheckGpsIsAvailable();
}public virtual bool CheckBluetoothIsAvailable()
{
return Bluetooth.IsAvailable;
}public virtual bool CheckBluetoothIsOn()
{
return Bluetooth.IsOn;
}public virtual bool NativeCheckCanConnect()
{
return true; //we have no checks provided by native sdk like HasPermissions etc.
}public virtual void OnCanConnect()
{
//normally could call some native code to update the internal sdk state etc
}#region CHECK BLE
protected string AppTitle;
protected Page MainPage;
public bool Initialized { get; protected set; }void ShowBluetoothOFFError()
{
Debug.WriteLine(ResStrings.AlertTurnOnBluetooth);
MainThread.BeginInvokeOnMainThread(() =>
{
MainPage.DisplayAlert(AppTitle, ResStrings.AlertTurnOnBluetooth, ResStrings.BtnOk);
});
}
void ShowGPSPermissionsError()
{
Debug.WriteLine(ResStrings.AlertNeedGpsPermissionsForBluetooth);
MainThread.BeginInvokeOnMainThread(() =>
{
MainPage.DisplayAlert(AppTitle, ResStrings.AlertNeedGpsPermissionsForBluetooth, ResStrings.BtnOk);
});
}
void ShowErrorGPSOff()
{
Debug.WriteLine(ResStrings.AlertNeedGpsOnForBluetooth);
MainThread.BeginInvokeOnMainThread(() =>
{
MainPage.DisplayAlert(AppTitle, ResStrings.AlertNeedGpsOnForBluetooth, ResStrings.BtnOk);
});
}void ShowBluetoothNotAvailableError()
{
Debug.WriteLine(ResStrings.AlertBluetoothUnsupported);
MainThread.BeginInvokeOnMainThread(() =>
{
MainPage.DisplayAlert(AppTitle, ResStrings.AlertBluetoothUnsupported, ResStrings.BtnOk);
});
}void ShowBluetoothPermissionsError()
{
Debug.WriteLine(ResStrings.AlertBluetoothPermissionsOff);
MainThread.BeginInvokeOnMainThread(() =>
{
MainPage.DisplayAlert(AppTitle, ResStrings.AlertBluetoothPermissionsOff, ResStrings.BtnOk);
});
}public async Task CheckCanConnectDisplayErrors()
{#if ANDROID
var status = await BluetoothPermissions.CheckBluetoothStatus();
if (status != PermissionStatus.Granted)
{
Tasks.StartTimerAsync(TimeSpan.FromMilliseconds(150), async () =>
{
MainThread.BeginInvokeOnMainThread(async () =>
{if (BluetoothPermissions.NeedGPS)
{
await MainPage.DisplayAlert(AppTitle, ResStrings.AlertNeedLocationForBluetooth, ResStrings.BtnOk);
}status = await BluetoothPermissions.RequestBluetoothAccess();
if (status == PermissionStatus.Granted)
{
//disabled, using android:usesPermissionFlags="neverForLocation"if (BluetoothPermissions.NeedGPS)
{
if (!CheckGpsIsAvailable())
{
ShowErrorGPSOff();
return;
}
}await CheckCanConnectDisplayErrors();
}
else
{
ShowBluetoothPermissionsError();
}
});
return false;
});return false;
}if (BluetoothPermissions.NeedGPS)
{
if (!CheckGpsIsAvailable())
{
ShowErrorGPSOff();
return false;
}
}#else
//Not androidif (DeviceInfo.DeviceType == DeviceType.Virtual) // simulator
return true;#if (IOS || MACCATALYST)
var create = CheckBluetoothIsAvailable(); //for to show permissions prompt
if (!NativeCheckCanConnect())
{
return false;
}
else
{
await Task.Delay(200); // loads ble status (on or off)
OnCanConnect();
}#else
// WINDOWS?..
#endif
#endif
if (CheckBluetoothIsAvailable())
{
if (CheckBluetoothIsOn())
{
return true;
}ShowBluetoothOFFError();
return false;}
else
{
ShowBluetoothNotAvailableError();
return false;
}return true;
}#endregion
#endregion
public event EventHandler OnDecoded;
public event EventHandler OnDecodedExtended;
public event EventHandler OnDecodedSettings;private string _LastSerial;
public string LastSerial
{
get { return _LastSerial; }
set
{
if (_LastSerial != value)
{
_LastSerial = value;
OnPropertyChanged();
}
}
}private string _LastName;
public string LastName
{
get { return _LastName; }
set
{
if (_LastName != value)
{
_LastName = value;
OnPropertyChanged();
}
}
}public void StopScanning()
{
if (CancelScan != null)
{
CancelScan.Cancel();
}
}protected CancellationTokenSource CancelScan { get; set; }
public async Task ScanForCompatibleDevices()
{
if (IsBusy)
{
throw new Exception("Already connecting");
}if (await CheckCanConnectDisplayErrors())
{try
{
CancelScan = new();FilterServiceUuids.Clear();
await
WithScanTimeout(8000)
.WithServiceUuid(MyKnownDeviceService).ScanAsync(CancelScan.Token);return true;
}
catch (Exception e)
{
Console.WriteLine(e);
}}
return false;
}
private string _Serial = "";
///
/// Device UID
///
public string Serial
{
get
{
return _Serial;
}
set
{
if (_Serial != value)
{
if (value != null)
{
value = value.Trim();
}
_Serial = value;
OnPropertyChanged();
OnPropertyChanged("SerialIsValid");
}
}
}public bool IsMonitoring
{
get
{
return _subscribed != null;
}
}private ConnectionState _ConnectionState;
///
/// Статус Bluetooth соединения
///
public ConnectionState ConnectionState
{
get { return _ConnectionState; }
set
{
if (_ConnectionState != value)
{
_ConnectionState = value;
OnPropertyChanged();
}
}
}public bool SerialIsValid
{
get
{
return Serial != null && Serial.Length == 36;
}
}public async Task FindDeviceAndConnect()
{
if (!SerialIsValid)
throw new Exception("Bad serial");Preferences.Set("Serial", Serial);
if (await CheckCanConnectDisplayErrors())
{
await ConnectToSerialDevice(true);
}
}private BleCharacteristicViewModel _subscribed;
///
/// We subsribe to a READ characteristing and will callback called when value changes there
///
///
///
///
public async Task StartMonitoring(BleCharacteristicViewModel characteristic, bool needThrow = false)
{
if (_subscribed != null)
StopMonitoring(_subscribed);try
{
characteristic.Info.ValueUpdated -= OnDataChanged;
characteristic.Info.ValueUpdated += OnDataChanged;await characteristic.Info.StartUpdatesAsync();
_subscribed = characteristic;
SetStatus($"Monitoring on");
//todo your upon connected device logic
return true;
}
catch (Exception e)
{
_subscribed = null;Trace.WriteLine(e);
SetStatus("Monitoring unsuppurted..");
if (needThrow)
{
OnPropertyChanged("IsMonitoring");
throw e;
}
}
finally
{
OnPropertyChanged("IsMonitoring");
}return false;
}private string _LastSentCommand = "none";
public string LastSentCommand
{
get { return _LastSentCommand; }
set
{
if (_LastSentCommand != value)
{
_LastSentCommand = value;
OnPropertyChanged();
}
}
}private string _LastSentData;
public string LastSentData
{
get { return _LastSentData; }
set
{
if (_LastSentData != value)
{
_LastSentData = value;
OnPropertyChanged();
}
}
}protected bool ChannelBusy { get; set; }
void ProcessDataReceived(byte[] bytes)
{try
{//todo your logic
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
finally
{
ChannelBusy = false;
}
}public void StopMonitoring(BleCharacteristicViewModel characteristic)
{_subscribed = null;
try
{
characteristic.Info.ValueUpdated -= OnDataChanged;
}
catch (Exception e)
{
Trace.WriteLine(e);
}SetStatus("Monitoring off");
OnPropertyChanged("IsMonitoring");
}public event EventHandler DataReceived;
///
/// DEBUG only
///
///
[Conditional("DEBUG")]
void SetStatus(string status)
{
Status = status;
#if WINDOWS
Trace.WriteLine(status);
#else
Console.WriteLine(status);
#endif
}private string _Status;
public string Status
{
get { return _Status; }
set
{
if (_Status != value)
{
_Status = value;
OnPropertyChanged();
Debug.WriteLine($"Status: {value}");
}
}
}private string _DataIn;
private BleCharacteristicViewModel _writeTo;public string DataIn
{
get { return _DataIn; }
set
{
if (_DataIn != value)
{
_DataIn = value;
OnPropertyChanged();
}
}
}private void OnDataChanged(object sender, CharacteristicUpdatedEventArgs args)
{
int total = 0;
try
{var bytes = args.Characteristic.Value;
total = bytes.Length;
SetStatus($"Received {total} bytes");
//Debug.WriteLine($"[BLE MONITOR] {Status}");
if (total == 0)
return;ProcessDataReceived(bytes);
DataReceived?.Invoke(this, bytes);
}
catch (Exception e)
{
SetStatus("Read error");Console.WriteLine(e);
DataIn = "";//$"Gor {total} vytes + Error: {e.ToString()}";
}
}
}```
Inside your viewmodel, you can now use it to scan for compatible devices and to connect, for example (consider this a pseudo-code):
```csharp
private SemaphoreSlim semaphoreConnector = new(1, 1);async Task Connect()
{
await semaphoreConnector.WaitAsync();try
{
if (!IsBusy && !Connector.IsBusy)
{
IsBusy = true;LastDeviceId = _preferences.Get("LastDevice", string.Empty);
if (string.IsNullOrEmpty(LastDeviceId)) //todo get from prefs last uid
{
//need find available devicesvar ok = await Connector.ScanForCompatibleDevices();
if (!ok)
{
throw new Exception("Scan failed");
}var devices = Connector.FoundDevices.Where(x => x.Device.Name != null
&& x.Device.Name.ToLower().Contains("brandname#")).DistinctBy(x => x.Device.Name).ToList();if (devices.Any())
{
if (devices.Count > 1)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
var options = devices.Select(x => new SelectableAction
{
Action = () =>
{
Connector.Serial = x.Id;
},
Id = x.Id,
Title = x.Device.Name
}).ToList();var selected = await _ui.PresentSelection(options) as SelectableAction;
if (selected != null)
{
try
{
selected?.Action?.Invoke();
await ConnectWithSetup();
}
catch (Exception e)
{
Debug.WriteLine(e);
LastDeviceId = null;
}
}});
return;
}Connector.Serial = devices.First().Id;
}
else
{
MainThread.BeginInvokeOnMainThread(async () =>
{
await _ui.Alert(ResStrings.VendorTitle, ResStrings.CompatibleDevicesNotFound);
});return;
}}
else
{
//try to connect to last device
Connector.Serial = LastDeviceId;
}await Connector.ConnectToSerialDevice(true);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
LastDeviceId = null;
}
finally
{
IsBusy = false;semaphoreConnector.Release();
UpdateUi();
}}
```
## Permissions
Connector contains code to request permissions.
How to setup permissions for your projects:Android manifest:
```xml
```
Windows `Package.appxmanifest`:
```xml
...
```
Mac Catalyst `Info.plist`:
```
NSBluetoothAlwaysUsageDescription
Bluetooth is required for this app to function properly
```iPhone `Info.plist`:
```
NSBluetoothPeripheralUsageDescription
Bluetooth is required for this app to function properly
NSBluetoothAlwaysUsageDescription
Bluetooth is required for this app to function properly
```