Parcourir la source

Stabilising PRS Digital Keys

frankvandenbos il y a 4 mois
Parent
commit
3f3104bd0d

+ 17 - 0
InABox.Avalonia.Platform.Android/Bluetooth/Android_BluetoothDevice.cs

@@ -0,0 +1,17 @@
+using Android.Bluetooth.LE;
+
+namespace InABox.Avalonia.Platform.Android;
+
+public class Android_BluetoothDevice(ScanResult scan, Guid[] availableservices, DateTime timestamp) : IBluetoothDevice, IDisposable
+{
+    public ScanResult Scan { get; } = scan;
+    public string ID { get; } = scan.Device?.Address ?? string.Empty;
+    public string Name { get; } = scan.ScanRecord?.DeviceName ?? "Unknown Device";
+    public Guid[] AvailableServices { get; } = availableservices;
+    public DateTime LastSeen { get; set; } = timestamp;
+
+    public void Dispose()
+    {
+        Scan.Dispose();
+    }
+}

+ 22 - 167
InABox.Avalonia.Platform.Android/Bluetooth.Android.cs → InABox.Avalonia.Platform.Android/Bluetooth/Android_ConnectedBluetoothDevice.cs

@@ -1,34 +1,19 @@
-using System.Collections.ObjectModel;
+using System.Text;
 using Android.Bluetooth;
 using Android.Bluetooth;
-using Android.Bluetooth.LE;
-using Android.Content;
-using Android.OS;
-using Android.Text.Style;
-using FluentResults;
-using InABox.Core;
-using Microsoft.Maui.ApplicationModel;
 
 
 namespace InABox.Avalonia.Platform.Android;
 namespace InABox.Avalonia.Platform.Android;
 
 
-public class Android_BluetoothDevice(ScanResult scan) : IBluetoothDevice, IDisposable
-{
-    public ScanResult Scan { get; } = scan;
-    public string ID { get; } = scan.Device?.Address ?? string.Empty;
-    public string Name { get; } = scan.ScanRecord?.DeviceName ?? "Unknown Device";
-
-    public void Dispose()
-    {
-        Scan.Dispose();
-    }
-}
-
 public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : BluetoothGattCallback,IConnectedBluetoothDevice
 public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : BluetoothGattCallback,IConnectedBluetoothDevice
 {
 {
 
 
     private BluetoothDevice _device = device;
     private BluetoothDevice _device = device;
     public string ID { get; } = device?.Address ?? string.Empty;
     public string ID { get; } = device?.Address ?? string.Empty;
     public string Name { get; } = device?.Name ?? "Unknown Device";
     public string Name { get; } = device?.Name ?? "Unknown Device";
-    
+    public DateTime LastSeen { get; set; } = DateTime.Now;
+
+    private Guid[] _availableServices = [];
+    public Guid[] AvailableServices => _availableServices;
+        
     private BluetoothGatt? _bluetoothGatt;
     private BluetoothGatt? _bluetoothGatt;
     
     
     private TaskCompletionSource<bool> _connectionTaskCompletionSource;
     private TaskCompletionSource<bool> _connectionTaskCompletionSource;
@@ -64,6 +49,7 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
     {
     {
         _serviceDiscoveryTaskCompletionSource = new TaskCompletionSource<bool>();
         _serviceDiscoveryTaskCompletionSource = new TaskCompletionSource<bool>();
         _bluetoothGatt?.DiscoverServices();
         _bluetoothGatt?.DiscoverServices();
+        _availableServices = _bluetoothGatt?.Services?.Select(x => Guid.Parse(x.Uuid?.ToString() ?? Guid.Empty.ToString())).ToArray() ?? [];
         return await _serviceDiscoveryTaskCompletionSource.Task;
         return await _serviceDiscoveryTaskCompletionSource.Task;
     }
     }
 
 
@@ -83,7 +69,7 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
         }
         }
     }
     }
     
     
-    public async Task<byte[]?> ReadAsync(Guid serviceid, Guid characteristicid)
+    public async Task<byte[]?> ReadBytesAsync(Guid serviceid, Guid characteristicid)
     {
     {
         byte[]? result = null;
         byte[]? result = null;
         if (_bluetoothGatt != null)
         if (_bluetoothGatt != null)
@@ -99,6 +85,12 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
         return result;
         return result;
     }
     }
 
 
+    public async Task<String?> ReadStringAsync(Guid serviceid, Guid characteristicid)
+    {
+        var data = await ReadBytesAsync(serviceid,characteristicid);
+        return data != null ? System.Text.Encoding.UTF8.GetString(data) : null;
+    }
+
     private async Task<byte[]?> ReadCharacteristicAsync(BluetoothGattCharacteristic characteristic)
     private async Task<byte[]?> ReadCharacteristicAsync(BluetoothGattCharacteristic characteristic)
     {
     {
         if (_bluetoothGatt != null)
         if (_bluetoothGatt != null)
@@ -132,7 +124,7 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
         }
         }
     }
     }
     
     
-    public async Task<bool> WriteAsync(Guid serviceid, Guid characteristicid, byte[] data)
+    public async Task<bool> WriteBytesAsync(Guid serviceid, Guid characteristicid, byte[] data)
     {
     {
         var result = false;
         var result = false;
         if (_bluetoothGatt != null)
         if (_bluetoothGatt != null)
@@ -149,13 +141,19 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
         return result;
         return result;
     }
     }
     
     
+    public async Task<bool> WriteStringAsync(Guid serviceid, Guid characteristicid, string data)
+    {
+        var encoded = Encoding.UTF8.GetBytes(data);
+        return await WriteBytesAsync(serviceid, characteristicid, encoded);
+    }
+    
     private async Task<bool> WriteCharacteristicAsync(BluetoothGattCharacteristic characteristic, byte[] data)
     private async Task<bool> WriteCharacteristicAsync(BluetoothGattCharacteristic characteristic, byte[] data)
     {
     {
         bool result = false;
         bool result = false;
         if (_bluetoothGatt != null)
         if (_bluetoothGatt != null)
         { 
         { 
             _writeTaskCompletionSource = new TaskCompletionSource<bool>();
             _writeTaskCompletionSource = new TaskCompletionSource<bool>();
-             _bluetoothGatt.WriteCharacteristic(characteristic, data, 2); 
+            _bluetoothGatt.WriteCharacteristic(characteristic, data, 2); 
             result = await _writeTaskCompletionSource.Task;
             result = await _writeTaskCompletionSource.Task;
         }
         }
         return result;
         return result;
@@ -195,147 +193,4 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
         Console.WriteLine("Resources released.");
         Console.WriteLine("Resources released.");
     }
     }
     
     
-}
-
-public class BluetoothScanManager : ScanCallback
-{
-    private readonly Action<ScanResult> _onDeviceFound;
-    private readonly Action _onScanStopped;
-
-    public BluetoothScanManager(Action<ScanResult> onDeviceFound, Action onScanStopped)
-    {
-        _onDeviceFound = onDeviceFound;
-        _onScanStopped = onScanStopped;
-    }
-
-    public override void OnScanResult(ScanCallbackType callbackType, ScanResult result)
-    {
-        base.OnScanResult(callbackType, result);
-        _onDeviceFound?.Invoke(result);
-    }
-
-    public override void OnScanFailed(ScanFailure errorCode)
-    {
-        base.OnScanFailed(errorCode);
-        _onScanStopped?.Invoke();
-        throw new Exception($"Scan failed with error code: {errorCode}");
-    }
-}
-
-public class Android_Bluetooth : IBluetooth
-{
-    public Logger? Logger { get; set; }
-
-    public CoreObservableCollection<IBluetoothDevice> Devices { get; private set; } = new CoreObservableCollection<IBluetoothDevice>();
-    
-    private readonly BluetoothLeScanner? _scanner;
-
-    public event EventHandler? Changed;
-
-    public Android_Bluetooth()
-    {
-        var _manager = Application.Context.GetSystemService(Context.BluetoothService) as BluetoothManager;
-        var _adapter = _manager?.Adapter;
-        _scanner = _adapter?.BluetoothLeScanner;
-    }
-    
-    public static async Task<bool> IsPermitted<TPermission>() where TPermission : Permissions.BasePermission, new()
-    {
-        try
-        {
-            PermissionStatus status = await Permissions.CheckStatusAsync<TPermission>();
-            if (status == PermissionStatus.Granted)
-                return true;
-            var request = await Permissions.RequestAsync<TPermission>();
-            return request == PermissionStatus.Granted;
-
-        }
-        catch (TaskCanceledException ex)
-        {
-            return false;
-        }
-    }
-
-    public async Task<bool> IsAvailable()
-    {
-        if (await IsPermitted<Permissions.Bluetooth>()) 
-            return _scanner != null;
-        return false;
-    }
-    BluetoothScanManager? _callback;
-    
-    public async Task<bool> StartScanningAsync(Guid serviceid)
-    {
-        if (await IsAvailable())
-        {
-            _callback = new BluetoothScanManager((d) => DoDeviceFound(d, serviceid), ScanStopped);
-            _scanner!.StartScan(_callback);
-            return true;
-        }
-        return false;
-    }
-
-    public async Task<bool> StopScanningAsync()
-    {
-        if (await IsAvailable())
-        {
-            if (_callback != null)
-            {
-                _scanner!.StopScan(_callback);
-                return true;
-            }
-        }
-        return false;
-    }
-    
-
-    private void DoDeviceFound(ScanResult device, Guid serviceid)
-    {
-        bool bMatch = true;
-        
-            bMatch = false;
-            if (device.ScanRecord?.ServiceUuids?.Any() == true)
-            {
-                foreach (var uuid in device.ScanRecord.ServiceUuids)
-                {
-                    if (Guid.TryParse(uuid.ToString(), out Guid guid))
-                        bMatch = bMatch || Guid.Equals(serviceid,guid);
-                }
-            }
-            
-        if (bMatch && !Devices.Any(x => x.ID == device.Device?.Address))
-        {
-            var abd = new Android_BluetoothDevice(device); 
-            Devices.Add(abd);
-        }
-
-    }
-
-    private void ScanStopped()
-    {
-        _callback = null;
-    }
-
-    public async Task<IConnectedBluetoothDevice?> Connect(IBluetoothDevice device)
-    {
-        if (device is Android_BluetoothDevice d && d.Scan.Device is BluetoothDevice bd)
-        {
-            var result = new Android_ConnectedBluetoothDevice(bd);
-            if (await result.ConnectAsync())
-            {
-                await result.DiscoverServicesAsync();
-                return result;
-            }
-        }
-        return null;
-    }
-    
-        
-    public async Task<bool> Disconnect(IConnectedBluetoothDevice device)
-    {
-        if (device is Android_ConnectedBluetoothDevice d)
-            d.Dispose();
-        
-        return await Task.FromResult(true);
-    }
 }
 }

+ 0 - 0
InABox.Avalonia.Platform.Android/BTConnector.cs → InABox.Avalonia.Platform.Android/Bluetooth/BTConnector.cs


+ 0 - 0
InABox.Avalonia.Platform.Android/BTManager.cs → InABox.Avalonia.Platform.Android/Bluetooth/BTManager.cs


+ 134 - 0
InABox.Avalonia.Platform.Android/Bluetooth/Bluetooth.Android.cs

@@ -0,0 +1,134 @@
+using Android.Bluetooth;
+using Android.Bluetooth.LE;
+using Android.Content;
+using InABox.Core;
+using Microsoft.Maui.ApplicationModel;
+
+namespace InABox.Avalonia.Platform.Android;
+
+public class Android_Bluetooth : IBluetooth
+{
+    public Logger? Logger { get; set; }
+
+    public CoreObservableCollection<IBluetoothDevice> Devices { get; private set; } = new CoreObservableCollection<IBluetoothDevice>();
+    
+    private readonly BluetoothLeScanner? _scanner;
+
+    public event EventHandler? Changed;
+
+    public Android_Bluetooth()
+    {
+        var _manager = Application.Context.GetSystemService(Context.BluetoothService) as BluetoothManager;
+        var _adapter = _manager?.Adapter;
+        _scanner = _adapter?.BluetoothLeScanner;
+        
+        Task.Run(() =>
+        {
+            while (true)
+            {
+                var stale = Devices.ToArray().Where(x => (x == null) || (x.LastSeen < DateTime.Now.Subtract(new TimeSpan(0, 0, 5))))
+                    .ToArray();
+                if (stale.Any())
+                    Devices.RemoveRange(stale);
+                Task.Delay(500);
+            }
+        });
+    }
+    
+    public static async Task<bool> IsPermitted<TPermission>() where TPermission : Permissions.BasePermission, new()
+    {
+        try
+        {
+            PermissionStatus status = await Permissions.CheckStatusAsync<TPermission>();
+            if (status == PermissionStatus.Granted)
+                return true;
+            var request = await Permissions.RequestAsync<TPermission>();
+            return request == PermissionStatus.Granted;
+
+        }
+        catch (TaskCanceledException ex)
+        {
+            return false;
+        }
+    }
+
+    public async Task<bool> IsAvailable()
+    {
+        if (await IsPermitted<Permissions.Bluetooth>()) 
+            return _scanner != null;
+        return false;
+    }
+    BluetoothScanManager? _callback;
+    
+    public async Task<bool> StartScanningAsync(Guid configServiceId)
+    {
+        if (await IsAvailable())
+        {
+            _callback = new BluetoothScanManager((d) => DoDeviceFound(d, configServiceId), ScanStopped);
+            _scanner!.StartScan(_callback);
+            return true;
+        }
+        return false;
+    }
+
+    public async Task<bool> StopScanningAsync()
+    {
+        if (await IsAvailable())
+        {
+            if (_callback != null)
+            {
+                _scanner!.StopScan(_callback);
+                return true;
+            }
+        }
+        return false;
+    }
+    
+
+    private void DoDeviceFound(ScanResult device, Guid configServiceId)
+    {
+
+        var abd = Devices.FirstOrDefault(x => x.ID == device.Device?.Address);
+        if (abd == null)
+        {
+            var services = device.ScanRecord?.ServiceUuids?
+                .Select(x => Guid.Parse(x.ToString()))
+                .Where(x => !x.ToString().ToUpper().EndsWith("-0000-1000-8000-00805F9B34FB") && configServiceId != x)
+                .ToArray() ?? [];
+
+            abd = new Android_BluetoothDevice(device, services, DateTime.Now);
+            Devices.Add(abd);
+        }
+        else
+            abd.LastSeen = DateTime.Now;
+
+    }
+
+    private void ScanStopped()
+    {
+        _callback = null;
+    }
+
+    public async Task<IConnectedBluetoothDevice?> Connect(IBluetoothDevice device)
+    {
+        if (device is Android_BluetoothDevice d && d.Scan.Device is BluetoothDevice bd)
+        {
+            var result = new Android_ConnectedBluetoothDevice(bd);
+            if (await result.ConnectAsync())
+            {
+                await result.DiscoverServicesAsync();
+                return result;
+            }
+        }
+        return null;
+    }
+    
+        
+    public async Task<bool> Disconnect(IConnectedBluetoothDevice device)
+    {
+        if (device is Android_ConnectedBluetoothDevice d)
+            d.Dispose();
+        
+        return await Task.FromResult(true);
+    }
+}

+ 28 - 0
InABox.Avalonia.Platform.Android/Bluetooth/BluetoothScanManager.cs

@@ -0,0 +1,28 @@
+using Android.Bluetooth.LE;
+
+namespace InABox.Avalonia.Platform.Android;
+
+public class BluetoothScanManager : ScanCallback
+{
+    private readonly Action<ScanResult> _onDeviceFound;
+    private readonly Action _onScanStopped;
+
+    public BluetoothScanManager(Action<ScanResult> onDeviceFound, Action onScanStopped)
+    {
+        _onDeviceFound = onDeviceFound;
+        _onScanStopped = onScanStopped;
+    }
+
+    public override void OnScanResult(ScanCallbackType callbackType, ScanResult result)
+    {
+        base.OnScanResult(callbackType, result);
+        _onDeviceFound?.Invoke(result);
+    }
+
+    public override void OnScanFailed(ScanFailure errorCode)
+    {
+        base.OnScanFailed(errorCode);
+        _onScanStopped?.Invoke();
+        throw new Exception($"Scan failed with error code: {errorCode}");
+    }
+}

+ 0 - 1
InABox.Avalonia.Platform.Android/InABox.Avalonia.Platform.Android.csproj

@@ -20,7 +20,6 @@
     <ItemGroup>
     <ItemGroup>
       <ProjectReference Include="..\..\..\..\..\development\inabox\inabox.logging.shared\InABox.Logging.Shared.csproj" />
       <ProjectReference Include="..\..\..\..\..\development\inabox\inabox.logging.shared\InABox.Logging.Shared.csproj" />
       <ProjectReference Include="..\InABox.Avalonia.Platform\InABox.Avalonia.Platform.csproj" />
       <ProjectReference Include="..\InABox.Avalonia.Platform\InABox.Avalonia.Platform.csproj" />
-      <ProjectReference Include="..\InABox.Avalonia\InABox.Avalonia.csproj" />
     </ItemGroup>
     </ItemGroup>
     
     
     <ItemGroup>
     <ItemGroup>

+ 0 - 111
InABox.Avalonia.Platform.Desktop/Desktop.Bluetooth.cs

@@ -1,111 +0,0 @@
-using System.Collections.ObjectModel;
-using BluetoothLENet;
-using InABox.Core;
-
-
-namespace InABox.Avalonia.Platform.Desktop;
-
-public class Desktop_BluetoothDevice(BLEDevice device) : IBluetoothDevice
-{
-    public BLEDevice Device { get; } = device;
-    public string ID { get; } = device.MacAddress ?? string.Empty;
-    public string Name { get; } = device.Native?.Name ?? "Unknown Device";
-}
-
-public class Desktop_ConnectedBluetoothDevice(BLEDevice device)
-    : Desktop_BluetoothDevice(device), IConnectedBluetoothDevice
-{
-    public async Task<bool> WriteAsync(Guid serviceid, Guid characteristicid, byte[] data)
-    {
-        var service = Device.Services.FirstOrDefault(x=>x.Native.Uuid == serviceid);
-        if (service != null)
-        {
-            var characteristic = service.Characteristics.FirstOrDefault(x=>x.Native.Uuid == characteristicid);
-            if (characteristic != null)
-                return await characteristic.WriteAsync(data);
-        }
-        return false;
-    }
-
-    public async Task<byte[]?> ReadAsync(Guid serviceid, Guid characteristicid)
-    {
-        var service = Device.Services.FirstOrDefault(x=>x.Native.Uuid == serviceid);
-        if (service != null)
-        {
-            var characteristic = service.Characteristics.FirstOrDefault(x=>x.Native.Uuid == characteristicid);
-            if (characteristic != null)
-                return await characteristic.ReadAsync();
-        }
-        return [];
-    }
-}
-
-public class Desktop_Bluetooth : IBluetooth
-{
-    public Logger? Logger { get; set; }
-
-    private BLE _adapter;
-    
-    public Action<IBluetoothDevice>? DeviceFound { get; set; }
-    public CoreObservableCollection<IBluetoothDevice> Devices { get; } = new();
-
-    public event EventHandler? Changed;
-    
-    public Desktop_Bluetooth()
-    {
-        Devices.CollectionChanged += (_,_) => Changed?.Invoke(this, EventArgs.Empty);
-        
-        _adapter = new BLE();
-        _adapter.Changed += (_,_) =>
-        {
-            var devices = _adapter.Devices.ToArray().Select(x=>new Desktop_BluetoothDevice(x));
-            Devices.ReplaceRange(devices);
-        };
-        
-        
-    }
-
-    public async Task<bool> IsAvailable()
-    {
-        return await Task.FromResult(true);   
-    }
-    
-    public async Task<bool> StartScanningAsync(Guid serviceId)
-    {
-        if (await IsAvailable())
-            return await _adapter.StartScanningAsync([serviceId]);
-        return false;
-    }
-
-    public async Task<bool> StopScanningAsync()
-    {
-        if (await IsAvailable())
-            return await _adapter.StopScanningAsync();
-        return false;
-    }
-
-    public async Task<IConnectedBluetoothDevice?> Connect(IBluetoothDevice device)
-    {
-        if (await IsAvailable())
-        {
-            if (device is Desktop_BluetoothDevice d)
-            {
-                var result = await _adapter.Connect(d.Device);
-                if (result == ConnectDeviceResult.Ok)
-                    return new Desktop_ConnectedBluetoothDevice(d.Device);
-            }
-        }
-        return null;
-        
-    }
-
-    public async Task<bool> Disconnect(IConnectedBluetoothDevice device)
-    {
-        if (await IsAvailable())
-        {
-            if (device is Desktop_BluetoothDevice d)
-                _adapter.Disconnect(d.Device);
-        }
-        return true;
-    }
-}

+ 0 - 24
InABox.Avalonia.Platform.Desktop/Desktop.PdfRenderer.cs

@@ -1,24 +0,0 @@
-using InABox.Core;
-using PDFtoImage;
-using SkiaSharp;
-
-namespace InABox.Avalonia.Platform.Desktop;
-
-public class Desktop_PdfRenderer : IPdfRenderer
-{
-    public byte[]? RenderPdf(byte[]? pdf, int page, int dpi)
-    {
-        if (pdf?.Any() != true)
-            return null;
-        
-        var result = Conversion.ToImage(pdf, page, options: new RenderOptions(Dpi: dpi));
-        using var ms = new MemoryStream();
-        result.Encode(ms, SKEncodedImageFormat.Jpeg, 65);
-        return ms.ToArray();
-    }
-
-    public Task<byte[]?> RenderPdfAsync(byte[]? pdf, int page, int dpi)
-        => Task.Run(() => RenderPdf(pdf, page, dpi));
-
-    public Logger? Logger { get; set; }
-}

+ 0 - 25
InABox.Avalonia.Platform.Desktop/InABox.Avalonia.Platform.Desktop.csproj

@@ -1,25 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-    <PropertyGroup>
-        <OutputType>Library</OutputType>
-        <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
-        <ImplicitUsings>enable</ImplicitUsings>
-        <Nullable>enable</Nullable>
-        <Platforms>x86;x64;arm64</Platforms>
-        <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
-        <UseWinUI>true</UseWinUI>
-        <WindowsPackaging>None</WindowsPackaging>
-        <EnableMsixTooling>true</EnableMsixTooling>
-    </PropertyGroup>
-
-    <ItemGroup>
-      <ProjectReference Include="..\..\3rdpartylibs\BluetoothLENet-master\BluetoothLeNet\BluetoothLeNet.csproj" />
-      <ProjectReference Include="..\InABox.Avalonia.Platform\InABox.Avalonia.Platform.csproj" />
-    </ItemGroup>
-
-    <ItemGroup>
-      <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
-      <PackageReference Include="PDFtoImage" Version="5.0.0" />
-    </ItemGroup>
-
-</Project>

+ 1 - 1
InABox.Avalonia.Platform/Bluetooth/DefaultBluetooth.cs

@@ -12,7 +12,7 @@ public class DefaultBluetooth : IBluetooth
         return await Task.FromResult(false);   
         return await Task.FromResult(false);   
     }
     }
     
     
-    public async Task<bool> StartScanningAsync(Guid serviceid)
+    public async Task<bool> StartScanningAsync(Guid configServiceid)
     {
     {
         return await Task.FromResult(false);
         return await Task.FromResult(false);
     }
     }

+ 10 - 3
InABox.Avalonia.Platform/Bluetooth/IBluetooth.cs

@@ -9,15 +9,22 @@ public interface IBluetoothDevice
      String ID { get; }
      String ID { get; }
      
      
      String Name { get; }
      String Name { get; }
+
+     Guid[] AvailableServices { get; }
+     
+     DateTime LastSeen { get; set; }
 }
 }
 
 
 public interface IConnectedBluetoothDevice : IBluetoothDevice
 public interface IConnectedBluetoothDevice : IBluetoothDevice
 {
 {
 
 
-     Task<bool> WriteAsync(Guid serviceid, Guid characteristicid, byte[] data);
+     Task<bool> WriteBytesAsync(Guid serviceid, Guid characteristicid, byte[] data);
+     
+     Task<bool> WriteStringAsync(Guid serviceid, Guid characteristicid, string data);
      
      
-     Task<byte[]?> ReadAsync(Guid serviceid, Guid characteristicid);
+     Task<byte[]?> ReadBytesAsync(Guid serviceid, Guid characteristicid);
      
      
+     Task<string?> ReadStringAsync(Guid serviceid, Guid characteristicid);
 }
 }
 
 
 public interface IBluetooth : ILoggable
 public interface IBluetooth : ILoggable
@@ -28,7 +35,7 @@ public interface IBluetooth : ILoggable
 
 
      //Task<IBluetoothDevice?> FindDevice(Guid serviceId, TimeSpan timeout);
      //Task<IBluetoothDevice?> FindDevice(Guid serviceId, TimeSpan timeout);
      
      
-     Task<bool> StartScanningAsync(Guid serviceId);
+     Task<bool> StartScanningAsync(Guid configServiceId);
      
      
      Task<bool> StopScanningAsync();
      Task<bool> StopScanningAsync();
      
      

+ 21 - 0
InABox.Avalonia/Components/CountdownTimer/AvaloniaCountdownTimer.cs

@@ -5,6 +5,7 @@ using Avalonia.Threading;
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Net;
 using System.Net;
+using System.Windows.Input;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Svg.Skia;
 using Avalonia.Svg.Skia;
 using InABox.Core;
 using InABox.Core;
@@ -36,6 +37,24 @@ public class CircularCountdownTimer : Control
         set => SetValue(IsActiveProperty, value);
         set => SetValue(IsActiveProperty, value);
     }
     }
     
     
+    public static readonly StyledProperty<ICommand> StartedProperty =
+        AvaloniaProperty.Register<CircularCountdownTimer, ICommand>(nameof(Started));
+
+    public ICommand Started
+    {
+        get => GetValue(StartedProperty);
+        set => SetValue(StartedProperty, value);
+    }
+    
+    public static readonly StyledProperty<ICommand> StoppedProperty =
+        AvaloniaProperty.Register<CircularCountdownTimer, ICommand>(nameof(Stopped));
+
+    public ICommand Stopped
+    {
+        get => GetValue(StoppedProperty);
+        set => SetValue(StoppedProperty, value);
+    }
+    
         
         
     public static readonly StyledProperty<IBrush> BackgroundProperty =
     public static readonly StyledProperty<IBrush> BackgroundProperty =
         AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(Background), Brushes.Transparent);
         AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(Background), Brushes.Transparent);
@@ -111,6 +130,7 @@ public class CircularCountdownTimer : Control
     {
     {
         _startTime = DateTime.Now;
         _startTime = DateTime.Now;
         _timer.Start();
         _timer.Start();
+        Started?.Execute(DataContext);
         InvalidateVisual();
         InvalidateVisual();
     }
     }
     
     
@@ -118,6 +138,7 @@ public class CircularCountdownTimer : Control
     {
     {
         _timer.Stop();
         _timer.Stop();
         _startTime = null;
         _startTime = null;
+        Stopped?.Execute(DataContext);
         InvalidateVisual();
         InvalidateVisual();
     }
     }
     
     

+ 4 - 4
InABox.Avalonia/Components/MenuPanel/AvaloniaMenuItem.cs

@@ -27,18 +27,18 @@ public partial class AvaloniaMenuItem : ObservableObject
     {
     {
     }
     }
 
 
-    public AvaloniaMenuItem(IImage? image, Action action)
+    public AvaloniaMenuItem(IImage? image, Func<Task<bool>> action)
         : this(image, () => null, action)
         : this(image, () => null, action)
     {
     {
     }
     }
 
 
-    public AvaloniaMenuItem(IImage? image, Func<CoreMenu<IImage?>?> build, Action? action = null)
+    public AvaloniaMenuItem(IImage? image, Func<CoreMenu<IImage>?> build, Func<Task<bool>>? action = null)
     {
     {
         Image = image; //LoadImage(image);
         Image = image; //LoadImage(image);
 
 
         TapCommand = new RelayCommand(() =>
         TapCommand = new RelayCommand(() =>
         {
         {
-            CoreMenu<IImage?>? menu = build();
+            CoreMenu<IImage>? menu = build();
             if (menu != null)
             if (menu != null)
             {
             {
                 var context = new ContextMenu();
                 var context = new ContextMenu();
@@ -48,7 +48,7 @@ public partial class AvaloniaMenuItem : ObservableObject
             }
             }
             else if (action != null)
             else if (action != null)
             {
             {
-                action();
+                _ = action();
             }
             }
         });
         });
 
 

+ 19 - 0
InABox.Avalonia/Converters/GuidToColorConverter.cs

@@ -0,0 +1,19 @@
+using Avalonia.Media;
+
+namespace InABox.Avalonia.Converters;
+
+public class GuidToColorConverter : AbstractConverter<Guid,IBrush>
+{
+        
+    public IBrush Empty{ get; set; }
+        
+    public IBrush Default { get; set; }
+        
+    protected override IBrush Convert(Guid value, object parameter = null)
+    {
+        return value != Guid.Empty
+            ? Default
+            : Empty;
+    }
+
+}

+ 6 - 0
InABox.Avalonia/DataModels/Shell.cs

@@ -9,6 +9,12 @@ namespace InABox.Avalonia
         where TParent : ICoreRepository
         where TParent : ICoreRepository
         where TEntity : Entity, IPersistent, IRemotable, new()
         where TEntity : Entity, IPersistent, IRemotable, new()
     {
     {
+        private object? _tag;
+        public object? Tag
+        {
+            get => _tag;
+            set => SetProperty(ref _tag, value);
+        }
         
         
         #region INotifyPropertyChanged
         #region INotifyPropertyChanged
         
         

+ 2 - 2
InABox.Avalonia/Navigation/Navigation.cs

@@ -7,8 +7,8 @@ namespace InABox.Avalonia;
 
 
 public interface IViewModelBase
 public interface IViewModelBase
 {
 {
-    void Activate();
-    void Deactivate();
+    Task Activate();
+    Task Deactivate();
     bool BackButtonVisible { get; set; }
     bool BackButtonVisible { get; set; }
     AvaloniaMenuItemCollection PrimaryMenu { get; set; }
     AvaloniaMenuItemCollection PrimaryMenu { get; set; }
     AvaloniaMenuItemCollection SecondaryMenu { get; set; }
     AvaloniaMenuItemCollection SecondaryMenu { get; set; }

+ 6 - 0
InABox.Avalonia/Theme/Converters.axaml

@@ -4,10 +4,16 @@
 
 
     <converters:StringToBooleanConverter x:Key="StringToBooleanConverter" />
     <converters:StringToBooleanConverter x:Key="StringToBooleanConverter" />
     
     
+    <converters:ObjectToBooleanConverter x:Key="ObjectToBooleanConverter" />
+    
     <converters:EmptyConverter x:Key="EmptyConverter" />
     <converters:EmptyConverter x:Key="EmptyConverter" />
     
     
     <converters:ByteArrayToImageSourceConverter x:Key="ByteArrayToImageSourceConverter"/>
     <converters:ByteArrayToImageSourceConverter x:Key="ByteArrayToImageSourceConverter"/>
     
     
     <converters:ByteArrayToImageSourceConverter x:Key="TransparentByteArrayToImageSourceConverter" Transparent="True" />
     <converters:ByteArrayToImageSourceConverter x:Key="TransparentByteArrayToImageSourceConverter" Transparent="True" />
+    
+    <converters:BooleanMatcher x:Key="MatchAllConverter" Comparison="EqualTo" Type="All" />
+    
+    <converters:BooleanMatcher x:Key="MatchAnyConverter" Comparison="EqualTo" Type="Any" />
 
 
 </ResourceDictionary>
 </ResourceDictionary>

+ 3 - 2
InABox.Core/CoreMenu/CoreMenu.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 
 
 namespace InABox.Core
 namespace InABox.Core
 {
 {
@@ -8,14 +9,14 @@ namespace InABox.Core
     {
     {
         public List<ICoreMenuItem> Items { get; } = new List<ICoreMenuItem>();
         public List<ICoreMenuItem> Items { get; } = new List<ICoreMenuItem>();
 
 
-        public CoreMenu<T> AddItem(string header, T? image, Action action)
+        public CoreMenu<T> AddItem(string header, T? image, Func<Task<bool>> action)
         {
         {
             var result = new CoreMenuItem<T>(header, image, action);
             var result = new CoreMenuItem<T>(header, image, action);
             Items.Add(result);
             Items.Add(result);
             return this;
             return this;
         }
         }
 
 
-        public CoreMenu<T> AddItem(string header, Action action)
+        public CoreMenu<T> AddItem(string header, Func<Task<bool>> action)
         {
         {
             var result = new CoreMenuItem<T>(header, null, action);
             var result = new CoreMenuItem<T>(header, null, action);
             Items.Add(result);
             Items.Add(result);

+ 3 - 2
InABox.Core/CoreMenu/CoreMenuHeader.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 
 
 namespace InABox.Core
 namespace InABox.Core
 {
 {
@@ -18,14 +19,14 @@ namespace InABox.Core
             Image = image;
             Image = image;
         }
         }
     
     
-        public CoreMenuHeader<T> AddItem(string header, T? image, Action action)
+        public CoreMenuHeader<T> AddItem(string header, T? image, Func<Task<bool>> action)
         {
         {
             var result = new CoreMenuItem<T>(header, image, action);
             var result = new CoreMenuItem<T>(header, image, action);
             Items.Add(result);
             Items.Add(result);
             return this;
             return this;
         }
         }
     
     
-        public CoreMenuHeader<T> AddItem(string header, Action action)
+        public CoreMenuHeader<T> AddItem(string header, Func<Task<bool>> action)
         {
         {
             var result = new CoreMenuItem<T>(header, null, action);
             var result = new CoreMenuItem<T>(header, null, action);
             Items.Add(result);
             Items.Add(result);

+ 3 - 2
InABox.Core/CoreMenu/CoreMenuItem.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Threading.Tasks;
 
 
 namespace InABox.Core
 namespace InABox.Core
 {
 {
@@ -6,9 +7,9 @@ namespace InABox.Core
     {
     {
         public string Header { get; set; }
         public string Header { get; set; }
         public T? Image { get; set; }
         public T? Image { get; set; }
-        public Action? Action { get; set; }
+        public Func<Task<bool>>? Action { get; set; }
 
 
-        public CoreMenuItem(string header, T? image = null, Action? action = null)
+        public CoreMenuItem(string header, T? image = null, Func<Task<bool>>? action = null)
         {
         {
             Header = header;
             Header = header;
             Image = image;
             Image = image;