From 55f9e5fe4fce0b9bc018e7a64c72ae066a0a800a Mon Sep 17 00:00:00 2001 From: dfgHiatus Date: Thu, 18 Dec 2025 11:39:47 -1000 Subject: [PATCH] Detect when USB is (dis)connected, updated displayed devices --- src/Baballonia/App.axaml.cs | 1 + src/Baballonia/Baballonia.csproj | 1 + src/Baballonia/Contracts/IUsbService.cs | 9 +++ src/Baballonia/Services/USBService.cs | 68 ++++++++++++++++++++++ src/Baballonia/Views/HomePageView.axaml.cs | 21 +++++++ 5 files changed, 100 insertions(+) create mode 100644 src/Baballonia/Contracts/IUsbService.cs create mode 100644 src/Baballonia/Services/USBService.cs diff --git a/src/Baballonia/App.axaml.cs b/src/Baballonia/App.axaml.cs index f0cc369d..6961f382 100644 --- a/src/Baballonia/App.axaml.cs +++ b/src/Baballonia/App.axaml.cs @@ -153,6 +153,7 @@ public override void OnFrameworkInitializationCompleted() services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } ConfigurePlatformServices.Invoke(services); diff --git a/src/Baballonia/Baballonia.csproj b/src/Baballonia/Baballonia.csproj index 159170ce..95156407 100644 --- a/src/Baballonia/Baballonia.csproj +++ b/src/Baballonia/Baballonia.csproj @@ -65,6 +65,7 @@ + diff --git a/src/Baballonia/Contracts/IUsbService.cs b/src/Baballonia/Contracts/IUsbService.cs new file mode 100644 index 00000000..7b7708d1 --- /dev/null +++ b/src/Baballonia/Contracts/IUsbService.cs @@ -0,0 +1,9 @@ +using System; + +namespace Baballonia.Contracts; + +public interface IUsbService +{ + public event Action OnUsbConnected; + public event Action OnUsbDisconnected; +} diff --git a/src/Baballonia/Services/USBService.cs b/src/Baballonia/Services/USBService.cs new file mode 100644 index 00000000..185d968d --- /dev/null +++ b/src/Baballonia/Services/USBService.cs @@ -0,0 +1,68 @@ +using System; +using Baballonia.Contracts; +using Usb.Events; + +namespace Baballonia.Services; + +public sealed class UsbService : IUsbService +{ + public event Action? OnUsbConnected; + public event Action? OnUsbDisconnected; + + private static readonly IUsbEventWatcher UsbEventWatcher = new UsbEventWatcher( + startImmediately: true, + addAlreadyPresentDevicesToList: true, + usePnPEntity: false, + includeTTY: true); + + private readonly TimeSpan _eventThrottleInterval = TimeSpan.FromSeconds(1); + private DateTime _lastEventTime = DateTime.MinValue; + private readonly object _eventLock = new(); + + public UsbService() + { + UsbEventWatcher.UsbDeviceAdded += UsbDeviceAdded; + UsbEventWatcher.UsbDeviceRemoved += UsbDeviceRemoved; + } + + private void UsbDeviceAdded(object? sender, UsbDevice? device) + { + if (device == null) + return; + + RateLimitedAction(device.DeviceName, OnUsbConnected); + } + + private void UsbDeviceRemoved(object? sender, UsbDevice? device) + { + if (device == null) + return; + + RateLimitedAction(device.DeviceName, OnUsbDisconnected); + } + + private void RateLimitedAction(string deviceName, Action? action) + { + lock (_eventLock) + { + var now = DateTime.UtcNow; + var timeSinceLastEvent = now - _lastEventTime; + + if (timeSinceLastEvent < _eventThrottleInterval) + { + return; + } + + _lastEventTime = now; + } + + action?.Invoke(deviceName); + } + + ~UsbService() + { + UsbEventWatcher.UsbDeviceAdded -= UsbDeviceAdded; + UsbEventWatcher.UsbDeviceRemoved -= UsbDeviceRemoved; + UsbEventWatcher.Dispose(); + } +} diff --git a/src/Baballonia/Views/HomePageView.axaml.cs b/src/Baballonia/Views/HomePageView.axaml.cs index a1f94074..a73109cf 100644 --- a/src/Baballonia/Views/HomePageView.axaml.cs +++ b/src/Baballonia/Views/HomePageView.axaml.cs @@ -6,11 +6,14 @@ using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform.Storage; +using Avalonia.Threading; using Avalonia.VisualTree; using Baballonia.Assets; using Baballonia.Contracts; using Baballonia.Helpers; +using Baballonia.Services; using Baballonia.ViewModels.SplitViewPane; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Baballonia.Views; @@ -26,6 +29,7 @@ public partial class HomePageView : UserControl private readonly IDeviceEnumerator _deviceEnumerator; private readonly ILocalSettingsService _localSettings; + private IUsbService _usbService; public HomePageView(IDeviceEnumerator deviceEnumerator, ILocalSettingsService localSettings) { @@ -116,6 +120,17 @@ public HomePageView(IDeviceEnumerator deviceEnumerator, ILocalSettingsService lo vm.SelectedCalibrationTextBlock = this.Find("SelectedCalibrationTextBlockColor")!; vm.SelectedCalibrationTextBlock.Text = Assets.Resources.Home_Eye_Calibration; + + if (!Utils.IsSupportedDesktopOS) return; + _usbService = Ioc.Default.GetService()!; + _usbService.OnUsbConnected += RefreshDevices; + _usbService.OnUsbDisconnected += RefreshDevices; + }; + Unloaded += (_, _) => + { + if (_usbService != null) return; + _usbService!.OnUsbConnected -= RefreshDevices; + _usbService!.OnUsbDisconnected -= RefreshDevices; }; } @@ -199,6 +214,12 @@ private void RefreshConnectedFaceDevices(object? sender, CancelEventArgs e) //vm.FaceCamera.UpdateCameraDropDown(); } + private async void RefreshDevices(string deviceName) + { + await Dispatcher.UIThread.InvokeAsync( + () => RefreshDevices(null, null!)); + } + private async void RefreshDevices(object? sender, RoutedEventArgs e) { if (DataContext is not HomePageViewModel vm) return;