diff --git a/.gitignore b/.gitignore index cec6728b..db8c84cb 100644 --- a/.gitignore +++ b/.gitignore @@ -346,7 +346,6 @@ healthchecksdb /DSHMC.schema.json /setup/DsHidMini Driver-SetupFiles /setup/DsHidMini Driver-cache -/ControlApp **/vcpkg_installed **/*.g.wxs /XInputBridge/exports diff --git a/ControlApp/App.xaml b/ControlApp/App.xaml new file mode 100644 index 00000000..efa55f68 --- /dev/null +++ b/ControlApp/App.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + diff --git a/ControlApp/App.xaml.cs b/ControlApp/App.xaml.cs new file mode 100644 index 00000000..899cba1f --- /dev/null +++ b/ControlApp/App.xaml.cs @@ -0,0 +1,116 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.IO; +using System.Reflection; +using System.Windows.Threading; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Nefarius.DsHidMini.ControlApp.Models; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Services; +using Nefarius.DsHidMini.ControlApp.ViewModels.Pages; +using Nefarius.DsHidMini.ControlApp.ViewModels.Windows; +using Nefarius.DsHidMini.ControlApp.Views.Pages; +using Nefarius.DsHidMini.ControlApp.Views.Windows; +using Nefarius.Utilities.Bluetooth; +using Nefarius.Utilities.DeviceManagement.PnP; + +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Sinks.File; + +using Wpf.Ui; + +namespace Nefarius.DsHidMini.ControlApp +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App + { + // The.NET Generic Host provides dependency injection, configuration, logging, and other services. + // https://docs.microsoft.com/dotnet/core/extensions/generic-host + // https://docs.microsoft.com/dotnet/core/extensions/dependency-injection + // https://docs.microsoft.com/dotnet/core/extensions/configuration + // https://docs.microsoft.com/dotnet/core/extensions/logging + private static readonly IHost _host = Host + .CreateDefaultBuilder() + .ConfigureAppConfiguration(c => { c.SetBasePath(Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)); }) + .ConfigureServices((context, services) => + { + services.AddHostedService(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton
(); + + Log.Logger = new LoggerConfiguration() + //.MinimumLevel.Debug() + .WriteTo.File(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "DsHidMini\\Log\\ControlAppLog.txt")) + .CreateLogger(); + + services.AddSerilog(Log.Logger); + }).Build(); + + /// + /// Gets registered service. + /// + /// Type of the service to get. + /// Instance of the service or . + public static T GetService() + where T : class + { + return _host.Services.GetService(typeof(T)) as T; + } + + /// + /// Occurs when the application is loading. + /// + private void OnStartup(object sender, StartupEventArgs e) + { + Log.Logger.Information("App startup"); + _host.Start(); + } + + /// + /// Occurs when the application is closing. + /// + private async void OnExit(object sender, ExitEventArgs e) + { + Log.Logger.Information("App exiting"); + await _host.StopAsync(); + _host.Dispose(); + } + + /// + /// Occurs when an exception is thrown by an application but not handled. + /// + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + // For more info see https://docs.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception?view=windowsdesktop-6.0 + } + } +} diff --git a/ControlApp/AssemblyInfo.cs b/ControlApp/AssemblyInfo.cs new file mode 100644 index 00000000..d575b147 --- /dev/null +++ b/ControlApp/AssemblyInfo.cs @@ -0,0 +1,15 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located +//(used if a resource is not found in the page, +// app, or any theme specific resource dictionaries) +)] diff --git a/ControlApp/Assets/wpfui-icon-1024.png b/ControlApp/Assets/wpfui-icon-1024.png new file mode 100644 index 00000000..b70c4ed5 Binary files /dev/null and b/ControlApp/Assets/wpfui-icon-1024.png differ diff --git a/ControlApp/Assets/wpfui-icon-256.png b/ControlApp/Assets/wpfui-icon-256.png new file mode 100644 index 00000000..6b5cf5d5 Binary files /dev/null and b/ControlApp/Assets/wpfui-icon-256.png differ diff --git a/ControlApp/ControlApp.csproj b/ControlApp/ControlApp.csproj new file mode 100644 index 00000000..ff0fe958 --- /dev/null +++ b/ControlApp/ControlApp.csproj @@ -0,0 +1,71 @@ + + + + WinExe + net7.0-windows10.0.17763.0 + app.manifest + wpfui-icon.ico + enable + enable + true + AnyCPU;x64 + 10.0.17763.0 + Nefarius.DsHidMini.ControlApp + true + 3.0.0 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + test.resx + + + Code + + + Code + + + + + + PublicResXFileCodeGenerator + test.Designer.cs + + + + diff --git a/ControlApp/FodyWeavers.xml b/ControlApp/FodyWeavers.xml new file mode 100644 index 00000000..63fc1484 --- /dev/null +++ b/ControlApp/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ControlApp/FodyWeavers.xsd b/ControlApp/FodyWeavers.xsd new file mode 100644 index 00000000..f3ac4762 --- /dev/null +++ b/ControlApp/FodyWeavers.xsd @@ -0,0 +1,26 @@ + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/ControlApp/Helpers/BooleanConverter.cs b/ControlApp/Helpers/BooleanConverter.cs new file mode 100644 index 00000000..817aca3c --- /dev/null +++ b/ControlApp/Helpers/BooleanConverter.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using System.Windows.Data; + +namespace Nefarius.DsHidMini.ControlApp.Helpers +{ + public class BooleanConverter : IValueConverter + { + public BooleanConverter(T trueValue, T falseValue) + { + True = trueValue; + False = falseValue; + } + + public T True { get; set; } + public T False { get; set; } + + public virtual object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool && ((bool)value) ? True : False; + } + + public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is T && EqualityComparer.Default.Equals((T)value, True); + } + } + + public sealed class BooleanToVisibilityConverter : BooleanConverter + { + public BooleanToVisibilityConverter() : + base(Visibility.Visible, Visibility.Collapsed) + { } + } + + +} diff --git a/ControlApp/Helpers/BooleanToReverseConverter.cs b/ControlApp/Helpers/BooleanToReverseConverter.cs new file mode 100644 index 00000000..81fdfcec --- /dev/null +++ b/ControlApp/Helpers/BooleanToReverseConverter.cs @@ -0,0 +1,17 @@ +using System.Globalization; +using System.Windows.Data; + +namespace Nefarius.DsHidMini.ControlApp.Helpers +{ + public class BooleanToReverseConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + + return !(bool?)value ?? true; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => !(value as bool?); + } +} \ No newline at end of file diff --git a/ControlApp/Helpers/EnumBindingSourceExtension.cs b/ControlApp/Helpers/EnumBindingSourceExtension.cs new file mode 100644 index 00000000..976238ab --- /dev/null +++ b/ControlApp/Helpers/EnumBindingSourceExtension.cs @@ -0,0 +1,53 @@ +using System.Windows.Markup; + +namespace Nefarius.DsHidMini.ControlApp.Helpers +{ + public class EnumBindingSourceExtension : MarkupExtension + { + private Type _enumType; + + public EnumBindingSourceExtension() + { + } + + public EnumBindingSourceExtension(Type enumType) + { + EnumType = enumType; + } + + public Type EnumType + { + get => _enumType; + set + { + if (value != _enumType) + { + if (null != value) + { + var enumType = Nullable.GetUnderlyingType(value) ?? value; + if (!enumType.IsEnum) + throw new ArgumentException("Type must be for an Enum."); + } + + _enumType = value; + } + } + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + if (null == _enumType) + throw new InvalidOperationException("The EnumType must be specified."); + + var actualEnumType = Nullable.GetUnderlyingType(_enumType) ?? _enumType; + var enumValues = Enum.GetValues(actualEnumType); + + if (actualEnumType == _enumType) + return enumValues; + + var tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1); + enumValues.CopyTo(tempArray, 1); + return tempArray; + } + } +} \ No newline at end of file diff --git a/ControlApp/Helpers/EnumDescriptionTypeConverter.cs b/ControlApp/Helpers/EnumDescriptionTypeConverter.cs new file mode 100644 index 00000000..91ae4a1d --- /dev/null +++ b/ControlApp/Helpers/EnumDescriptionTypeConverter.cs @@ -0,0 +1,37 @@ +using System.ComponentModel; +using System.Globalization; + +namespace Nefarius.DsHidMini.ControlApp.Helpers +{ + public class EnumDescriptionTypeConverter : EnumConverter + { + public EnumDescriptionTypeConverter(Type type) + : base(type) + { + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, + Type destinationType) + { + if (destinationType == typeof(string)) + { + if (value != null) + { + var fi = value.GetType().GetField(value.ToString()); + if (fi != null) + { + var attributes = + (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); + return attributes.Length > 0 && !string.IsNullOrEmpty(attributes[0].Description) + ? attributes[0].Description + : value.ToString(); + } + } + + return string.Empty; + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} \ No newline at end of file diff --git a/ControlApp/Helpers/EnumToBooleanConverter.cs b/ControlApp/Helpers/EnumToBooleanConverter.cs new file mode 100644 index 00000000..b2e73150 --- /dev/null +++ b/ControlApp/Helpers/EnumToBooleanConverter.cs @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Globalization; +using System.Windows.Data; + +namespace Nefarius.DsHidMini.ControlApp.Helpers +{ + internal class EnumToBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is not String enumString) + { + throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); + } + + if (!Enum.IsDefined(typeof(Wpf.Ui.Appearance.ApplicationTheme), value)) + { + throw new ArgumentException("ExceptionEnumToBooleanConverterValueMustBeAnEnum"); + } + + var enumValue = Enum.Parse(typeof(Wpf.Ui.Appearance.ApplicationTheme), enumString); + + return enumValue.Equals(value); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is not String enumString) + { + throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); + } + + return Enum.Parse(typeof(Wpf.Ui.Appearance.ApplicationTheme), enumString); + } + } +} diff --git a/ControlApp/Helpers/LocalizedDescriptionAtribute.cs b/ControlApp/Helpers/LocalizedDescriptionAtribute.cs new file mode 100644 index 00000000..b7a3ef69 --- /dev/null +++ b/ControlApp/Helpers/LocalizedDescriptionAtribute.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; + +namespace Nefarius.DsHidMini.ControlApp.Helpers +{ + public class LocalizedDescriptionAttribute : DescriptionAttribute + { + ResourceManager _resourceManager; + string _resourceKey; + public LocalizedDescriptionAttribute(string resourceKey, Type resourceType) + { + _resourceManager = new ResourceManager(resourceType); + _resourceKey = resourceKey; + } + + public override string Description + { + get + { + string description = _resourceManager.GetString(_resourceKey); + return string.IsNullOrWhiteSpace(description) ? string.Format("[[{0}]]", _resourceKey) : description; + } + } + } +} diff --git a/ControlApp/Helpers/SettingsGroupsDataTemplateSelector.cs b/ControlApp/Helpers/SettingsGroupsDataTemplateSelector.cs new file mode 100644 index 00000000..3c985ef4 --- /dev/null +++ b/ControlApp/Helpers/SettingsGroupsDataTemplateSelector.cs @@ -0,0 +1,34 @@ +using System.Windows.Controls; +using Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings; + +namespace Nefarius.DsHidMini.ControlApp.Helpers +{ + internal class SettingsGroupsDataTemplateSelector : DataTemplateSelector + { + public override DataTemplate + SelectTemplate(object item, DependencyObject container) + { + if(item != null) + { + if (GroupSettingsTemplateIndex.TryGetValue(item.GetType(), out string templateKey)) + { + return Application.Current.TryFindResource(templateKey) as DataTemplate; + } + } + return null; + } + + public Dictionary GroupSettingsTemplateIndex = new Dictionary() + { + { typeof(HidModeSettingsViewModel), "Template_Unique_All"}, + { typeof(LedsSettingsViewModel), "Template_LEDsSettings"}, + { typeof(WirelessSettingsViewModel), "Template_WirelessSettings"}, + { typeof(SticksSettingsViewModel), "Template_SticksDeadZone"}, + { typeof(GeneralRumbleSettingsViewModel), "Template_RumbleBasicFunctions"}, + { typeof(OutputReportSettingsViewModel), "Template_OutputRateControl"}, + { typeof(LeftMotorRescalingSettingsViewModel), "Template_RumbleHeavyStrRescale"}, + { typeof(AltRumbleModeSettingsViewModel), "Template_RumbleVariableLightEmuTuning"}, + + }; + } +} diff --git a/ControlApp/Helpers/VisibilityPerHidModeConverter.cs b/ControlApp/Helpers/VisibilityPerHidModeConverter.cs new file mode 100644 index 00000000..ce3cf3ba --- /dev/null +++ b/ControlApp/Helpers/VisibilityPerHidModeConverter.cs @@ -0,0 +1,52 @@ +using System.Globalization; +using System.Windows.Data; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.Helpers +{ + public class VisibilityPerHidModeConverter : IValueConverter + { + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + + int VisibilityPerHIDModeFlags = System.Convert.ToInt32((string)parameter, 2); + SettingsContext context = (SettingsContext)value; + int amountToBitShift = 0; + + switch (context) + { + case SettingsContext.SDF: + amountToBitShift = 0; + break; + case SettingsContext.GPJ: + amountToBitShift = 1; + break; + case SettingsContext.SXS: + amountToBitShift = 2; + break; + case SettingsContext.DS4W: + amountToBitShift = 3; + break; + case SettingsContext.XInput: + amountToBitShift = 4; + break; + case SettingsContext.General: + amountToBitShift = 5; + break; + case SettingsContext.Global: + amountToBitShift = 6; + break; + default: + return false; + } + + return (((VisibilityPerHIDModeFlags >> amountToBitShift) & 1U) == 1) ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/ControlApp/Models/AppConfig.cs b/ControlApp/Models/AppConfig.cs new file mode 100644 index 00000000..73c91405 --- /dev/null +++ b/ControlApp/Models/AppConfig.cs @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +namespace Nefarius.DsHidMini.ControlApp.Models +{ + public class AppConfig + { + public string ConfigurationsFolder { get; set; } + + public string AppPropertiesFileName { get; set; } + } +} diff --git a/ControlApp/Models/ApplicationConfiguration.cs b/ControlApp/Models/ApplicationConfiguration.cs new file mode 100644 index 00000000..802107c2 --- /dev/null +++ b/ControlApp/Models/ApplicationConfiguration.cs @@ -0,0 +1,62 @@ +namespace Nefarius.DsHidMini.ControlApp.Models +{ + /// + /// Global settings of the UI tool (stored in %AppData%). + /// + public class ApplicationConfiguration + { + /// + /// Implicitly loads configuration from file. + /// + private static readonly Lazy AppConfigLazy = + new Lazy(() => JsonApplicationConfiguration + .Load( + GlobalConfigFileName, + true, + false)); + + /// + /// JSON (and schema) file name holding global configuration values. + /// + public static string GlobalConfigFileName => "ControlApp"; + + /// + /// True if a log file should be generated, false otherwise. + /// + public bool IsLoggingEnabled { get; set; } = false; + + /// + /// True if check for new version happens on startup, false otherwise. + /// + public bool IsUpdateCheckEnabled { get; set; } = true; + + /// + /// If true, downloads genuine OUI list and compares controller MAC against. + /// + public bool IsGenuineCheckEnabled { get; set; } = true; + + /// + /// Whether user has acknowledged the donation dialog. + /// + public bool HasAcknowledgedDonationDialog { get; set; } = false; + + /// + /// Singleton instance of app configuration. + /// + public static ApplicationConfiguration Instance => AppConfigLazy.Value; + + /// + /// Write changes to file. + /// + public void Save() + { + // + // Store (modified) configuration to disk + // + JsonApplicationConfiguration.Save( + GlobalConfigFileName, + this, + false); + } + } +} \ No newline at end of file diff --git a/ControlApp/Models/Drivers/BthPS3FilterDriver.cs b/ControlApp/Models/Drivers/BthPS3FilterDriver.cs new file mode 100644 index 00000000..9f757ebf --- /dev/null +++ b/ControlApp/Models/Drivers/BthPS3FilterDriver.cs @@ -0,0 +1,172 @@ +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Storage.FileSystem; +using Nefarius.DsHidMini.ControlApp.Models.Util; + +namespace Nefarius.DsHidMini.ControlApp.Models.Drivers +{ + public static class BthPS3FilterDriver + { + private const uint IOCTL_BTHPS3PSM_ENABLE_PSM_PATCHING = 0x002AAC04; + private const uint IOCTL_BTHPS3PSM_DISABLE_PSM_PATCHING = 0x002AAC08; + private const uint IOCTL_BTHPS3PSM_GET_PSM_PATCHING = 0x002A6C0C; + + private static readonly string BTHPS3PSM_CONTROL_DEVICE_PATH = "\\\\.\\BthPS3PSMControl"; + + private static string ErrorMessage => + "BthPS3 filter driver access failed. Is Bluetooth turned on? Are the drivers installed?"; + + /// + /// True if filter driver is currently loaded and operational, false otherwise. + /// + public static bool IsFilterAvailable + { + get + { + if (!BluetoothHelper.IsBluetoothRadioAvailable) + return false; + + using var handle = PInvoke.CreateFile( + BTHPS3PSM_CONTROL_DEVICE_PATH, + FILE_ACCESS_FLAGS.FILE_GENERIC_READ | FILE_ACCESS_FLAGS.FILE_GENERIC_WRITE, + FILE_SHARE_MODE.FILE_SHARE_READ | FILE_SHARE_MODE.FILE_SHARE_WRITE, + null, + FILE_CREATION_DISPOSITION.OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, null + ); + + var error = (WIN32_ERROR)Marshal.GetLastWin32Error(); + + return error is WIN32_ERROR.ERROR_SUCCESS or WIN32_ERROR.ERROR_ACCESS_DENIED; + } + } + + /// + /// Gets or sets current filter patching state. + /// + public static unsafe bool IsFilterEnabled + { + get + { + if (!BluetoothHelper.IsBluetoothRadioAvailable) + return false; + + using var handle = PInvoke.CreateFile( + BTHPS3PSM_CONTROL_DEVICE_PATH, + FILE_ACCESS_FLAGS.FILE_GENERIC_READ | FILE_ACCESS_FLAGS.FILE_GENERIC_WRITE, + FILE_SHARE_MODE.FILE_SHARE_READ | FILE_SHARE_MODE.FILE_SHARE_WRITE, + null, + FILE_CREATION_DISPOSITION.OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, null + ); + + if (handle.IsInvalid) + throw new Exception(ErrorMessage); + + var payloadBuffer = Marshal.AllocHGlobal(Marshal.SizeOf()); + var payload = new BTHPS3PSM_GET_PSM_PATCHING { DeviceIndex = 0 }; + + try + { + Marshal.StructureToPtr(payload, payloadBuffer, false); + + PInvoke.DeviceIoControl( + handle, + IOCTL_BTHPS3PSM_GET_PSM_PATCHING, + payloadBuffer.ToPointer(), + (uint)Marshal.SizeOf(), + payloadBuffer.ToPointer(), + (uint)Marshal.SizeOf(), + null, + null + ); + + payload = Marshal.PtrToStructure(payloadBuffer); + } + finally + { + Marshal.FreeHGlobal(payloadBuffer); + } + + return payload.IsEnabled > 0; + } + set + { + using var handle = PInvoke.CreateFile( + BTHPS3PSM_CONTROL_DEVICE_PATH, + FILE_ACCESS_FLAGS.FILE_GENERIC_READ | FILE_ACCESS_FLAGS.FILE_GENERIC_WRITE, + FILE_SHARE_MODE.FILE_SHARE_READ | FILE_SHARE_MODE.FILE_SHARE_WRITE, + null, + FILE_CREATION_DISPOSITION.OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, null + ); + + if (handle.IsInvalid) + throw new Exception(ErrorMessage); + + var payloadEnableBuffer = Marshal.AllocHGlobal(Marshal.SizeOf()); + var payloadEnable = new BTHPS3PSM_ENABLE_PSM_PATCHING { DeviceIndex = 0 }; + var payloadDisableBuffer = Marshal.AllocHGlobal(Marshal.SizeOf()); + var payloadDisable = new BTHPS3PSM_DISABLE_PSM_PATCHING { DeviceIndex = 0 }; + + try + { + Marshal.StructureToPtr(payloadEnable, payloadEnableBuffer, false); + Marshal.StructureToPtr(payloadDisable, payloadDisableBuffer, false); + + if (value) + PInvoke.DeviceIoControl( + handle, + IOCTL_BTHPS3PSM_ENABLE_PSM_PATCHING, + payloadEnableBuffer.ToPointer(), + (uint)Marshal.SizeOf(), + null, + 0, + null, + null + ); + else + PInvoke.DeviceIoControl( + handle, + IOCTL_BTHPS3PSM_DISABLE_PSM_PATCHING, + payloadDisableBuffer.ToPointer(), + (uint)Marshal.SizeOf(), + null, + 0, + null, + null + ); + } + finally + { + Marshal.FreeHGlobal(payloadEnableBuffer); + Marshal.FreeHGlobal(payloadDisableBuffer); + } + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct BTHPS3PSM_ENABLE_PSM_PATCHING + { + public uint DeviceIndex; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct BTHPS3PSM_DISABLE_PSM_PATCHING + { + public uint DeviceIndex; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)] + private struct BTHPS3PSM_GET_PSM_PATCHING + { + public uint DeviceIndex; + + public readonly uint IsEnabled; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0xC8)] + public readonly string SymbolicLinkName; + } + } +} \ No newline at end of file diff --git a/ControlApp/Models/Drivers/BthPS3ProfileDriver.cs b/ControlApp/Models/Drivers/BthPS3ProfileDriver.cs new file mode 100644 index 00000000..8ad2ca71 --- /dev/null +++ b/ControlApp/Models/Drivers/BthPS3ProfileDriver.cs @@ -0,0 +1,32 @@ +using Microsoft.Win32; + +namespace Nefarius.DsHidMini.ControlApp.Models.Drivers +{ + public static class BthPS3ProfileDriver + { + private static string ParametersPath => "SYSTEM\\CurrentControlSet\\Services\\BthPS3\\Parameters"; + + /// + /// Gets or sets the RawPDO setting. + /// + public static bool RawPDO + { + get + { + using (var key = Registry.LocalMachine.OpenSubKey(ParametersPath)) + { + if (int.TryParse(key?.GetValue("RawPDO").ToString(), out var result)) return result > 0; + + return false; + } + } + set + { + using (var key = Registry.LocalMachine.OpenSubKey(ParametersPath, true)) + { + key?.SetValue("RawPDO", value ? 1 : 0, RegistryValueKind.DWord); + } + } + } + } +} \ No newline at end of file diff --git a/ControlApp/Models/Drivers/DsHidMiniDriver.cs b/ControlApp/Models/Drivers/DsHidMiniDriver.cs new file mode 100644 index 00000000..32ecd024 --- /dev/null +++ b/ControlApp/Models/Drivers/DsHidMiniDriver.cs @@ -0,0 +1,85 @@ +using System.ComponentModel; +using Nefarius.DsHidMini.ControlApp.Helpers; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; +using Nefarius.DsHidMini.ControlApp.Resources; +using Nefarius.Utilities.DeviceManagement.PnP; + +namespace Nefarius.DsHidMini.ControlApp.Models.Drivers +{ + public static class DsHidMiniDriver + { + /// + /// Interface GUID common to all devices the DsHidMini driver supports. + /// + public static Guid DeviceInterfaceGuid => Guid.Parse("{399ED672-E0BD-4FB3-AB0C-4955B56FB86A}"); + + #region Read-only properties + + /// + /// Unified Device Property exposing current battery status. + /// + public static DevicePropertyKey BatteryStatusProperty => CustomDeviceProperty.CreateCustomDeviceProperty( + Guid.Parse("{3FECF510-CC94-4FBE-8839-738201F84D59}"), 2, + typeof(byte)); + + public static DevicePropertyKey LastPairingStatusProperty => CustomDeviceProperty.CreateCustomDeviceProperty( + Guid.Parse("{3FECF510-CC94-4FBE-8839-738201F84D59}"), 3, + typeof(int)); + + public static DevicePropertyKey LastHostRequestStatusProperty => CustomDeviceProperty.CreateCustomDeviceProperty( + Guid.Parse("{3FECF510-CC94-4FBE-8839-738201F84D59}"), 5, + typeof(int)); + + #endregion + + #region Common device properties + + public static DevicePropertyKey HidDeviceModeProperty => CustomDeviceProperty.CreateCustomDeviceProperty( + Guid.Parse("{6D293077-C3D6-4062-9597-BE4389404C02}"), 2, + typeof(byte)); + + public static DevicePropertyKey HostAddressProperty => CustomDeviceProperty.CreateCustomDeviceProperty( + Guid.Parse("{0xa92f26ca, 0xeda7, 0x4b1d, {0x9d, 0xb2, 0x27, 0xb6, 0x8a, 0xa5, 0xa2, 0xeb}}"), 1, + typeof(ulong)); + + public static DevicePropertyKey DeviceAddressProperty => CustomDeviceProperty.CreateCustomDeviceProperty( + Guid.Parse("{0x2bd67d8b, 0x8beb, 0x48d5, {0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a}}"), 1, + typeof(string)); + + public static DevicePropertyKey BluetoothLastConnectedTimeProperty => + CustomDeviceProperty.CreateCustomDeviceProperty( + Guid.Parse("{0x2bd67d8b, 0x8beb, 0x48d5, {0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a}}"), 11, + typeof(DateTimeOffset)); + + #endregion + } + + /// + /// Battery status values. + /// + [TypeConverter(typeof(EnumDescriptionTypeConverter))] + public enum DsBatteryStatus : byte + { + [Description("Unknown")] Unknown = 0x00, + [Description("Dying")] Dying = 0x01, + [Description("Low")] Low = 0x02, + [Description("Medium")] Medium = 0x03, + [Description("High")] High = 0x04, + [Description("Full")] Full = 0x05, + [Description("Charging")] Charging = 0xEE, + [Description("Charged")] Charged = 0xEF + } + + /// + /// HID device emulation modes. + /// + [TypeConverter(typeof(EnumDescriptionTypeConverter))] + public enum DsHidDeviceMode : byte + { + [Description("SDF (PCSX2)")] SDF = 0x01, + [Description("GPJ (Separated pressure)")] GPJ = 0x02, + [Description("SXS (Steam, RPCS3)")] SXS = 0x03, + [Description("DS4Windows")] DS4W = 0x04, + [Description("XInput")] XInput = 0x05 + } +} \ No newline at end of file diff --git a/ControlApp/Models/DshmConfigManager/DeviceData.cs b/ControlApp/Models/DshmConfigManager/DeviceData.cs new file mode 100644 index 00000000..c9229218 --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/DeviceData.cs @@ -0,0 +1,30 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; +using Newtonsoft.Json; + +namespace Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager +{ + public class DeviceData + { + public string DeviceMac { get; set; } = "0000000000"; + public string CustomName { get; set; } = "DualShock 3"; + public Guid GuidOfProfileToUse { get; set; } = ProfileData.DefaultGuid; + public BluetoothPairingMode BluetoothPairingMode { get; set; } = BluetoothPairingMode.Auto; + public string? PairingAddress { get; set; } = ""; + + [JsonIgnore] // PairOnHotReload should only be enabled temporarely to prevent pairing requests from being repeteadly sent on hot-reload + public bool PairOnHotReload { get; set; } = false; + public SettingsModes SettingsMode { get; set; } = SettingsModes.Global; + + /// + /// Settings used when Device is in Custom Mode + /// + public DeviceSettings Settings { get; set; } = new(); + + + + public DeviceData(string deviceMac) + { + DeviceMac = deviceMac; + } + } +} \ No newline at end of file diff --git a/ControlApp/Models/DshmConfigManager/DeviceSettings.cs b/ControlApp/Models/DshmConfigManager/DeviceSettings.cs new file mode 100644 index 00000000..80bf6e24 --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/DeviceSettings.cs @@ -0,0 +1,373 @@ +using System.Text.Json.Serialization; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig.Enums; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; +using Button = Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums.Button; +using LEDsMode = Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums.LEDsMode; +using PressureMode = Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums.PressureMode; + +namespace Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager +{ + public class ButtonsCombo + { + private int holdTime; + + public bool IsEnabled { get; set; } + public int HoldTime + { + get => holdTime; + set => holdTime = (value >= 0) ? value : 0; + } + + public Button[] ButtonCombo { get; init; } = new Button[3]; + + public ButtonsCombo() { } + + public ButtonsCombo(ButtonsCombo comboToCopy) + { + copyCombo(comboToCopy); + } + + public void copyCombo(ButtonsCombo comboToCopy) + { + IsEnabled = comboToCopy.IsEnabled; + HoldTime = comboToCopy.HoldTime; + for(int i = 0; i < ButtonCombo.Length; i++) + { + ButtonCombo[i] = comboToCopy.ButtonCombo[i]; + } + } + + } + + public class DeviceSettings + { + + public HidModeSettings HidMode { get; set; } = new(); + public LedsSettings LEDs { get; set; } = new(); + public WirelessSettings Wireless { get; set; } = new(); + public SticksSettings Sticks { get; set; } = new(); + public GeneralRumbleSettings GeneralRumble { get; set; } = new(); + public OutputReportSettings OutputReport { get; set; } = new(); + public LeftMotorRescalingSettings LeftMotorRescaling { get; set; } = new(); + public AltRumbleModeSettings AltRumbleAdjusts { get; set; } = new(); + + public DeviceSettings() + { + this.ResetToDefault(); + } + + public void ResetToDefault() + { + HidMode.ResetToDefault(); + LEDs.ResetToDefault(); + Wireless.ResetToDefault(); + Sticks.ResetToDefault(); + GeneralRumble.ResetToDefault(); + OutputReport.ResetToDefault(); + LeftMotorRescaling.ResetToDefault(); + AltRumbleAdjusts.ResetToDefault(); + } + + public static void CopySettings(DeviceSettings destiny, DeviceSettings source) + { + HidModeSettings.CopySettings(destiny.HidMode,source.HidMode); + LedsSettings.CopySettings(destiny.LEDs, source.LEDs); + WirelessSettings.CopySettings(destiny.Wireless, source.Wireless); + SticksSettings.CopySettings(destiny.Sticks, source.Sticks); + GeneralRumbleSettings.CopySettings(destiny.GeneralRumble, source.GeneralRumble); + OutputReportSettings.CopySettings(destiny.OutputReport, source.OutputReport); + LeftMotorRescalingSettings.CopySettings(destiny.LeftMotorRescaling, source.LeftMotorRescaling); + AltRumbleModeSettings.CopySettings(destiny.AltRumbleAdjusts, source.AltRumbleAdjusts); + } + } + + + public abstract class DeviceSubSettings + { + public abstract void ResetToDefault(); + } + + public class HidModeSettings : DeviceSubSettings + { + public SettingsContext SettingsContext { get; set; } = SettingsContext.XInput; + public PressureMode PressureExposureMode { get; set; } = PressureMode.Default; + public DPadMode DPadExposureMode { get; set; } = DPadMode.HAT; + public bool IsLEDsAsXInputSlotEnabled { get; set; } = false; + public bool PreventRemappingConflictsInSXSMode { get; set; } = false; + public bool PreventRemappingConflictsInDS4WMode { get; set; } = false; + public bool AllowAppsToOverrideLEDsInSXSMode { get; set; } = false; + + public override void ResetToDefault() + { + CopySettings(this, new()); + } + + public static void CopySettings(HidModeSettings destiny, HidModeSettings source) + { + destiny.SettingsContext = source.SettingsContext; + destiny.PressureExposureMode = source.PressureExposureMode; + destiny.DPadExposureMode = source.DPadExposureMode; + destiny.IsLEDsAsXInputSlotEnabled = source.IsLEDsAsXInputSlotEnabled; + destiny.PreventRemappingConflictsInDS4WMode = source.PreventRemappingConflictsInDS4WMode; + destiny.PreventRemappingConflictsInSXSMode = source.PreventRemappingConflictsInSXSMode; + destiny.AllowAppsToOverrideLEDsInSXSMode = source.AllowAppsToOverrideLEDsInSXSMode; + } + } + + public class LedsSettings : DeviceSubSettings + { + public LEDsMode LeDMode { get; set; } = LEDsMode.BatteryIndicatorPlayerIndex; + public bool AllowExternalLedsControl { get; set; } = false; + public All4LEDsCustoms LEDsCustoms { get; set; } = new(); + + + public override void ResetToDefault() + { + CopySettings(this, new()); + } + + public static void CopySettings(LedsSettings destiny, LedsSettings source) + { + destiny.LeDMode = source.LeDMode; + destiny.AllowExternalLedsControl = source.AllowExternalLedsControl; + destiny.LEDsCustoms.CopyLEDsCustoms(source.LEDsCustoms); + } + + public class All4LEDsCustoms + { + public singleLEDCustoms[] LED_x_Customs = new singleLEDCustoms[4]; + public All4LEDsCustoms() + { + for (int i = 0; i < LED_x_Customs.Length; i++) + { + LED_x_Customs[i] = new(i); + } + } + + public void CopyLEDsCustoms(All4LEDsCustoms customsToCopy) + { + for (int i = 0; i < LED_x_Customs.Length; i++) + { + LED_x_Customs[i].CopyCustoms(customsToCopy.LED_x_Customs[i]); + } + } + + public void ResetLEDsCustoms() + { + for (int i = 0; i < LED_x_Customs.Length; i++) + { + LED_x_Customs[i].Reset(); + } + } + + + + public class singleLEDCustoms + { + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public int LEDIndex { get; } + public bool IsLedEnabled { get; set; } = false; + public bool UseLEDEffects { get; set; } = false; + + public byte Duration { get; set; } = 0x00; + public int CycleDuration { get; set; } = 0x4000; + public byte OnPeriodCycles { get; set; } = 0xFF; + public byte OffPeriodCycles { get; set; } = 0xFF; + public singleLEDCustoms(int ledIndex) + { + this.LEDIndex = ledIndex; + IsLedEnabled = LEDIndex == 0 ? true : false; + } + + internal void Reset() + { + CopyCustoms(new(LEDIndex)); + } + + public void CopyCustoms(singleLEDCustoms copySource) + { + this.IsLedEnabled = copySource.IsLedEnabled; + this.UseLEDEffects = copySource.UseLEDEffects; + this.Duration = copySource.Duration; + this.CycleDuration = copySource.CycleDuration; + this.OnPeriodCycles = copySource.OnPeriodCycles; + this.OffPeriodCycles = copySource.OffPeriodCycles; + } + } + } + + } + + public class WirelessSettings : DeviceSubSettings + { + public bool IsWirelessIdleDisconnectEnabled { get; set; } = true; + public int WirelessIdleDisconnectTime { get; set; } = 300000; // 5 minutes + public ButtonsCombo QuickDisconnectCombo { get; set; } = new() + { + IsEnabled = true, + HoldTime = 1000, + ButtonCombo = new[] {Button.PS, Button.R1, Button.L1}, + }; + + public override void ResetToDefault() + { + CopySettings(this,new()); + } + + public static void CopySettings(WirelessSettings destiny, WirelessSettings source) + { + destiny.IsWirelessIdleDisconnectEnabled = source.IsWirelessIdleDisconnectEnabled; + destiny.WirelessIdleDisconnectTime = source.WirelessIdleDisconnectTime; + destiny.QuickDisconnectCombo.copyCombo(source.QuickDisconnectCombo); + } + } + + public class SticksSettings : DeviceSubSettings + { + public StickData LeftStickData { get; set; } = new(); + public StickData RightStickData { get; set; } = new(); + + public override void ResetToDefault() + { + LeftStickData.Reset(); + RightStickData.Reset(); + } + + public static void CopySettings(SticksSettings destiny, SticksSettings source) + { + destiny.LeftStickData.CopyStickDataFromOtherStick(source.LeftStickData); + destiny.RightStickData.CopyStickDataFromOtherStick(source.RightStickData); + } + + public class StickData + { + public bool IsDeadZoneEnabled { get; set; } = true; + public int DeadZone { get; set; } = 0; + + public bool InvertXAxis { get; set; } = false; + public bool InvertYAxis { get; set; } = false; + + public StickData() + { + + } + + public void Reset() + { + CopyStickDataFromOtherStick(new()); + } + + public void CopyStickDataFromOtherStick(StickData copySource) + { + this.IsDeadZoneEnabled = copySource.IsDeadZoneEnabled; + this.DeadZone = copySource.DeadZone; + this.InvertXAxis = copySource.InvertXAxis; + this.InvertYAxis = copySource.InvertYAxis; + } + + } + + } + + public class GeneralRumbleSettings : DeviceSubSettings + { + + // -------------------------------------------- DEFAULT SETTINGS END + + public bool IsLeftMotorDisabled { get; set; } = false; + public bool IsRightMotorDisabled { get; set; } = false; + + public bool IsAltRumbleModeEnabled { get; set; } = false; + public bool AlwaysStartInNormalMode { get; set; } = false; + public bool IsAltModeToggleButtonComboEnabled { get; set; } = false; + public ButtonsCombo AltModeToggleButtonCombo { get; set; } = new() + { + IsEnabled = false, + HoldTime = 1000, + ButtonCombo = new[] {Button.PS, Button.Select, Button.Select}, + }; + + public override void ResetToDefault() + { + CopySettings(this, new()); + } + + public static void CopySettings(GeneralRumbleSettings destiny, GeneralRumbleSettings source) + { + destiny.IsAltRumbleModeEnabled = source.IsAltRumbleModeEnabled; + destiny.IsLeftMotorDisabled = source.IsLeftMotorDisabled; + destiny.IsRightMotorDisabled = source.IsRightMotorDisabled; + destiny.AlwaysStartInNormalMode = source.IsAltModeToggleButtonComboEnabled; + destiny.AltModeToggleButtonCombo.copyCombo(source.AltModeToggleButtonCombo); + } + } + + public class OutputReportSettings : DeviceSubSettings + { + public bool IsOutputReportRateControlEnabled { get; set; } = true; + public int MaxOutputRate { get; set; } = 150; + public bool IsOutputReportDeduplicatorEnabled { get; set; } = false; + + public override void ResetToDefault() + { + CopySettings(this, new()); + } + + public static void CopySettings(OutputReportSettings destiny, OutputReportSettings source) + { + destiny.IsOutputReportDeduplicatorEnabled = source.IsOutputReportDeduplicatorEnabled; + destiny.IsOutputReportRateControlEnabled = source.IsOutputReportRateControlEnabled; + destiny.MaxOutputRate = source.MaxOutputRate; + } + } + + public class LeftMotorRescalingSettings : DeviceSubSettings + { + public bool IsLeftMotorStrRescalingEnabled { get; set; } = true; + public int LeftMotorStrRescalingUpperRange { get; set; } = 255; + public int LeftMotorStrRescalingLowerRange { get; set; } = 64; + + + public override void ResetToDefault() + { + CopySettings(this, new()); + } + + public static void CopySettings(LeftMotorRescalingSettings destiny, LeftMotorRescalingSettings source) + { + destiny.IsLeftMotorStrRescalingEnabled = source.IsLeftMotorStrRescalingEnabled; + destiny.LeftMotorStrRescalingLowerRange = source.LeftMotorStrRescalingLowerRange; + destiny.LeftMotorStrRescalingUpperRange = source.LeftMotorStrRescalingUpperRange; + } + } + + public class AltRumbleModeSettings : DeviceSubSettings + { + public int ForcedRightMotorHeavyThreshold { get; set; } = 230; + public int ForcedRightMotorLightThreshold { get; set; } = 230; + public bool IsForcedRightMotorHeavyThreasholdEnabled { get; set; } = false; + public bool IsForcedRightMotorLightThresholdEnabled { get; set; } = false; + + public int RightRumbleConversionUpperRange { get; set; } = 140; + public int RightRumbleConversionLowerRange { get; set; } = 1; + + + public override void ResetToDefault() + { + CopySettings(this, new()); + } + + public static void CopySettings(AltRumbleModeSettings destiny, AltRumbleModeSettings source) + { + destiny.RightRumbleConversionLowerRange = source.RightRumbleConversionLowerRange; + destiny.RightRumbleConversionUpperRange = source.RightRumbleConversionUpperRange; + // Right rumble (light) threshold + destiny.IsForcedRightMotorLightThresholdEnabled = source.IsForcedRightMotorLightThresholdEnabled; + destiny.ForcedRightMotorLightThreshold = source.ForcedRightMotorLightThreshold; + // Left rumble (Heavy) threshold + destiny.IsForcedRightMotorHeavyThreasholdEnabled = source.IsForcedRightMotorHeavyThreasholdEnabled; + destiny.ForcedRightMotorHeavyThreshold = source.ForcedRightMotorHeavyThreshold; + } + } +} diff --git a/ControlApp/Models/DshmConfigManager/DshmConfig/DshmConfig.cs b/ControlApp/Models/DshmConfigManager/DshmConfig/DshmConfig.cs new file mode 100644 index 00000000..5596a8a2 --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/DshmConfig/DshmConfig.cs @@ -0,0 +1,179 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig.Enums; + +using Serilog; + +using static Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig.DshmDeviceSettings; + +namespace Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig +{ + + /// + /// DsHidMini driver settings for a specific Device (or global) + /// + public class DshmDeviceSettings + { + public HidDeviceMode? HIDDeviceMode { get; set; }// = DSHM_HidDeviceModes.DS4Windows; + public bool? DisableAutoPairing { get; set; } + + public DevicePairingMode? DevicePairingMode { get; set; } + public bool? PairOnHotReload { get; set; } // = false; + public string? CustomPairingAddress { get; set; } + public bool? DisableWirelessIdleTimeout { get; set; }// = false; + public bool? IsOutputRateControlEnabled { get; set; }// = true; + public byte? OutputRateControlPeriodMs { get; set; }// = 150; + public bool? IsOutputDeduplicatorEnabled { get; set; }// = false; + public double? WirelessIdleTimeoutPeriodMs { get; set; }// = 300000; + public bool? IsQuickDisconnectComboEnabled { get; set; } = true; + public ButtonCombo QuickDisconnectCombo { get; set; } = new(); + + + [JsonIgnore] + public DshmHidModeSettings ContextSettings { get; set; } = new(); + public DshmHidModeSettings? SDF => HIDDeviceMode == HidDeviceMode.SDF ? ContextSettings : null; + public DshmHidModeSettings? GPJ => HIDDeviceMode == HidDeviceMode.GPJ ? ContextSettings : null; + public DshmHidModeSettings? SXS => HIDDeviceMode == HidDeviceMode.SXS ? ContextSettings : null; + public DshmHidModeSettings? DS4Windows => HIDDeviceMode == HidDeviceMode.DS4Windows ? ContextSettings : null; + public DshmHidModeSettings? XInput => HIDDeviceMode == HidDeviceMode.XInput ? ContextSettings : null; + + public DshmDeviceSettings() + { + + } + + public class DeadZoneSettings + { + public bool? Apply + { + get; + set; + } + public byte? PolarValue { get; set; }// = 10.0; + + } + + public class HeavyRescaleSettings + { + public bool? IsEnabled { get; set; }// = true; + public byte? RescaleMinRange { get; set; }// = 64; + public byte? RescaleMaxRange { get; set; }// = 255; + } + + public class AlternativeModeSettings + { + public bool? IsEnabled { get; set; }// = false; + public byte? RescaleMinRange { get; set; }// = 1; + public byte? RescaleMaxRange { get; set; }// = 160; + public ForcedRightAdjusts ForcedRight { get; set; } = new(); + public ButtonCombo? ToggleCombo { get; set; } = new(); // = DSHM_QuickDisconnectCombo.PS_R1_L1 + } + + public class ButtonCombo + { + public bool? IsEnabled { get; set; } + public double? HoldTime { get; set; } + public int? Button1 { get; set; } + public int? Button2 { get; set; } + public int? Button3 { get; set; } + } + + public class ForcedRightAdjusts + { + public bool? IsHeavyThresholdEnabled { get; set; }// = false; + public byte? HeavyThreshold { get; set; }// = 230; + public bool? IsLightThresholdEnabled { get; set; }// = false; + public byte? LightThreshold { get; set; }// = 230; + } + + public class AllRumbleSettings + { + public bool? DisableLeft { get; set; }// = false; + public bool? DisableRight { get; set; }// = false; + public HeavyRescaleSettings HeavyRescale { get; set; } = new(); + public AlternativeModeSettings AlternativeMode { get; set; } = new(); + } + + public class SingleLEDCustoms + { + public byte? TotalDuration { get; set; }// = 255; + public ushort? BasePortionDuration { get; set; }// = 255; + public byte? OffPortionMultiplier { get; set; }// = 0; + public byte? OnPortionMultiplier { get; set; }// = 255; + } + + public class AllLEDSettings + { + public LEDsMode? Mode { get; set; }// = DSHM_LEDsModes.BatteryIndicatorPlayerIndex; + public DSHM_LEDsAuthority? Authority { get; set; } + public LEDsCustoms CustomPatterns { get; set; } = new(); + } + + public class LEDsCustoms + { + public byte? LEDFlags { get; set; } // = 0x2; + public SingleLEDCustoms Player1 { get; set; } = new(); + public SingleLEDCustoms Player2 { get; set; } = new(); + public SingleLEDCustoms Player3 { get; set; } = new(); + public SingleLEDCustoms Player4 { get; set; } = new(); + } + + public class AxesFlipping + { + public bool? LeftX { get; set; } + public bool? LeftY { get; set; } + public bool? RightX { get; set; } + public bool? RightY { get; set; } + } + } + + /// + /// DsHidMini driver settings related only to a given Hid Device Mode + /// + public class DshmHidModeSettings + { + [JsonIgnore] + public HidDeviceMode? HIDDeviceMode { get; set; } + public PressureMode? PressureExposureMode { get; set; }// = DSHM_PressureModes.Default; + public DPadExposureMode? DPadExposureMode { get; set; }// = DSHM_DPadExposureModes.Default; + public DeadZoneSettings DeadZoneLeft { get; set; } = new(); + public DeadZoneSettings DeadZoneRight { get; set; } = new(); + public AllRumbleSettings RumbleSettings { get; set; } = new(); + public AllLEDSettings LEDSettings { get; set; } = new(); + public AxesFlipping FlipAxis { get; set; } = new(); + } + + /// + /// A class representing the DsHidMini configuration disk file + /// + public class DshmConfiguration + { + public DshmDeviceSettings Global { get; set; } = new(); + public List Devices { get; set; } = new(); + + /// + /// Updates the DsHidMini configuration file on disk accordingly to this object's settings + /// + /// If the update was successfully + public bool ApplyConfiguration() + { + Log.Logger.Debug("Converting DsHidMini configuration object to configuration file."); + return DshmConfigSerialization.UpdateDsHidMiniConfigFile(this); + + } + } + + /// + /// class representing a DsHidMini specific device data, containing its MAC address and Settings + /// + public class DshmDeviceData + { + public string DeviceAddress { get; set; } + public DshmDeviceSettings DeviceSettings { get; set; } = new(); + + } + + + +} + diff --git a/ControlApp/Models/DshmConfigManager/DshmConfig/DshmConfigSerialization.cs b/ControlApp/Models/DshmConfigManager/DshmConfig/DshmConfigSerialization.cs new file mode 100644 index 00000000..3ec9d2b5 --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/DshmConfig/DshmConfigSerialization.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Threading.Tasks; + +using Serilog; + +namespace Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig +{ + internal static class DshmConfigSerialization + { + private const string DISK = @"C:\"; + + private const string DSHM_FOLDER_PATH_IN_DISK = @"ProgramData\DsHidMini\"; + public static string DshmFolderFullPath { get; } = $@"{DISK}{DSHM_FOLDER_PATH_IN_DISK}"; + + public static string DshmFileNameWithoutExtension { get; } = $@"DsHidMini"; + + public static string DshmConfigFileFormat { get; } = $@".json"; + + + + /// + /// Attempts to update the DsHidMini configuration file on disk by serializing a DshmConfiguration object into the proper dshidmini v3 format + /// + /// The DshmConfiguration object containing the desired dshidmini v3 settings + /// True if the update occurred successfully, false otherwise + public static bool UpdateDsHidMiniConfigFile(DshmConfiguration dshmConfig) + { + Log.Logger.Debug("Starting serialization of DsHidMini config object and saving to disk"); + try + { + string dshmSerializedConfiguration = JsonSerializer.Serialize(dshmConfig, DshmConfigSerializerOptions); + System.IO.Directory.CreateDirectory(DshmFolderFullPath); + System.IO.File.WriteAllText($@"{DshmFolderFullPath}{DshmFileNameWithoutExtension}{DshmConfigFileFormat}", dshmSerializedConfiguration); + return true; + } + catch (Exception e) + { + Log.Logger.Error(e, "Serialization or saving to disk failed."); + return false; + } + } + + public static JsonSerializerOptions DshmConfigSerializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true, + + Converters = + { + new JsonStringEnumConverter(), + new DshmConfigCustomJsonConverter(), + } + }; + + /// + /// A custom converter necessary to serialiaze a DshmConfiguration object into the proper DsHidMini v3 format. + /// Removes starting and ending brackets from the Devices list and makes each object in the list have it's object name be their MAC address. + /// + public class DshmConfigCustomJsonConverter : JsonConverter + { + public override DshmConfiguration Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write( + Utf8JsonWriter writer, DshmConfiguration instance, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WritePropertyName(nameof(instance.Global)); + var serializedGlobal = JsonSerializer.Serialize(instance.Global, options); + writer.WriteRawValue(serializedGlobal); + + //JsonSerializer.Serialize(writer, new { instance.Global }, options); + + writer.WritePropertyName(nameof(instance.Devices)); + writer.WriteStartObject(); + foreach (DshmDeviceData device in instance.Devices) + { + if (string.IsNullOrEmpty(device.DeviceAddress?.Trim())) + throw new JsonException("Expected non-null, non-empty Name"); + writer.WritePropertyName(device.DeviceAddress); + + var serializedCustomSettings = JsonSerializer.Serialize(device.DeviceSettings, options); + writer.WriteRawValue(serializedCustomSettings); + } + writer.WriteEndObject(); + + writer.WriteEndObject(); + + } + } + } +} diff --git a/ControlApp/Models/DshmConfigManager/DshmConfig/Enums/DSHMDriverEnums.cs b/ControlApp/Models/DshmConfigManager/DshmConfig/Enums/DSHMDriverEnums.cs new file mode 100644 index 00000000..218c56ac --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/DshmConfig/Enums/DSHMDriverEnums.cs @@ -0,0 +1,68 @@ +namespace Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig.Enums +{ + public enum HidDeviceMode + { + SDF, + GPJ, + SXS, + DS4Windows, + XInput, + } + + public enum DevicePairingMode + { + Auto, + Custom, + Disabled, + } + + public enum PressureMode + { + Digital, + Analogue, + Default, + } + + public enum DPadExposureMode + { + HAT, + IndividualButtons, + Default, + } + + public enum LEDsMode + { + BatteryIndicatorPlayerIndex, + BatteryIndicatorBarGraph, + CustomPattern, + } + + public enum Button + { + None, + PS, + START, + SELECT, + R1, + L1, + R2, + L2, + R3, + L3, + Triangle, + Circle, + Cross, + Square, + Up, + Right, + Down, + Left, + } + + public enum DSHM_LEDsAuthority + { + Automatic, + Driver, + Application, + } +} diff --git a/ControlApp/Models/DshmConfigManager/DshmConfigManager.cs b/ControlApp/Models/DshmConfigManager/DshmConfigManager.cs new file mode 100644 index 00000000..3fab63f8 --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/DshmConfigManager.cs @@ -0,0 +1,333 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; +using Serilog; +using Serilog.Core; + +namespace Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager +{ + /// + /// Class for managing user's dshidmini settings and applying them to the DsHidMini Configuration File + /// + public class DshmConfigManager + { + + /// + /// Singleton instace of the DshmConfigManager's user data + /// + private static readonly DshmConfigManagerUserData dshmManagerUserData = DshmConfigManagerUserData.Instance; + /// + /// Raised when the DsHidMini Configuraton File on disk is updated + /// + public event EventHandler DshmConfigurationUpdated; + /// + /// Raised when the GlobalProfile is updated + /// + public event EventHandler GlobalProfileUpdated; + + + // ----------------------------------------------------------- PROPERTIES + + /// + /// Profile used for all new controllers and those configured to use Global Settings Mode. + /// Reverts back to the Default Profile if the profile it's set to does not exist + /// + public ProfileData GlobalProfile + { + get + { + ProfileData gp = GetProfile(dshmManagerUserData.GlobalProfileGuid); + if (gp == null) + { + Log.Logger.Debug("Global profile set to non-existing profile"); + Log.Logger.Debug("Reverting Global profile to default profile."); + dshmManagerUserData.GlobalProfileGuid = ProfileData.DefaultGuid; + GlobalProfileUpdated?.Invoke(this, new()); + gp = ProfileData.DefaultProfile; + } + return gp; + } + set + { + Log.Logger.Debug($"Setting profile {value.ProfileName} as Global Profile"); + dshmManagerUserData.GlobalProfileGuid = value.ProfileGuid; + GlobalProfileUpdated?.Invoke(this,new()); + } + + } + + + // ----------------------------------------------------------- CONSTRUCTOR + + public DshmConfigManager() + { + FixDevicesWithBlankProfiles(); + } + + /// + /// Dshm Config Manager's User Data, containing the profile set as Global and Devices/Profiles datas + /// + private class DshmConfigManagerUserData + { + /// + /// Implicitly loads configuration from file. + /// + private static readonly Lazy AppConfigLazy = + new Lazy(() => JsonDshmUserData + .Load( + GlobalUserDataFileName, + true, + GlobalUserDataDirectory)); + + /// + /// Singleton instance of app configuration. + /// + public static DshmConfigManagerUserData Instance => AppConfigLazy.Value; + + /// + /// Configuration file name + /// + [JsonIgnore] + public static string GlobalUserDataFileName => "DshmUserData"; + + public static string GlobalUserDataFolderName => "ControlApp"; + + public static string GlobalUserDataDirectory + { + get + { + var commonFolder = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + return Path.Combine(commonFolder, GlobalUserDataFolderName); + } + } + /// + /// Guid of the profile set as global + /// + public Guid GlobalProfileGuid { get; set; } = ProfileData.DefaultGuid; + /// + /// List of profiles datas + /// + public List Profiles { get; set; } = new(); + /// + /// List of known devices datas + /// + public List Devices { get; set; } = new(); + + + /// + /// Write changes to file. + /// + public void Save() + { + // + // Store (modified) configuration to disk + // + JsonDshmUserData.Save( + GlobalUserDataFileName, + this, + GlobalUserDataDirectory); + } + + } + + // ----------------------------------------------------------- METHODS + + public void SaveChanges() + { + Log.Logger.Information("Saving DsHidMini User Data to disk."); + dshmManagerUserData.Save(); + } + + /// + /// Links Devices Datas back to the default profile if the profile they are set to use doesn't exist anymore, + /// also reverting them to the Global Settings Mode if in Profile Setting Mode + /// + private void FixDevicesWithBlankProfiles() + { + foreach(DeviceData device in dshmManagerUserData.Devices) + { + if(GetProfile(device.GuidOfProfileToUse) == null) + { + Log.Logger.Information($"Device {device.DeviceMac} linked to non-existing profile. Reverting link to default profile."); + device.GuidOfProfileToUse = ProfileData.DefaultGuid; + if (device.SettingsMode == SettingsModes.Profile) + { + Log.Logger.Information($"Device {device.DeviceMac} was in Profile Settings Mode while using a non-existing profile. Setting device back to Global Settings. "); + device.SettingsMode = SettingsModes.Global; + } + + } + } + } + + /// + /// If it exists, returns the Profile Data identified by the given GUID + /// + /// + /// The profile data of the given GUID if it exists, null otherwise + public ProfileData? GetProfile(Guid profileGuid) + { + ProfileData profile = null; + + foreach(ProfileData p in GetListOfProfilesWithDefault()) + { + if(p.ProfileGuid == profileGuid) + { + profile = p; + break; + } + } + + if (profile == null) + { + Log.Logger.Debug($"No profile with GUID {profileGuid} found."); + } + return profile; + } + + /// + /// Saves the DshmConfigManager configuration to disk and updates DsHidMini configuration file + /// + public void SaveChangesAndUpdateDsHidMiniConfigFile() + { + dshmManagerUserData.Save(); + ApplySettings(); + } + + /// + /// Updates the DsHidMini configuration file on disk based on the global profile and each device's settings + /// + public void ApplySettings() + { + Log.Information("Updating DsHidMini configuration based on DsHidMini User Data"); + Log.Debug("Building DsHidMini configuration object based on DsHidMini User Data"); + var dshmConfiguration = new DshmConfiguration(); + DshmManagerToDriverConversion.ConvertDeviceSettingsToDriverFormat(GlobalProfile.Settings, dshmConfiguration.Global); + + foreach(DeviceData dev in dshmManagerUserData.Devices) + { + var dshmDeviceData = new DshmDeviceData(); + dshmDeviceData.DeviceAddress = dev.DeviceMac; + + // Disable BT auto-pairing if in Disabled BT Pairing Mode + dshmDeviceData.DeviceSettings.DisableAutoPairing = + (dev.BluetoothPairingMode == BluetoothPairingMode.Disabled) ? true : false; + + dshmDeviceData.DeviceSettings.DevicePairingMode = DshmManagerToDriverConversion.PairingModeManagerToDriver[dev.BluetoothPairingMode]; + + dshmDeviceData.DeviceSettings.PairOnHotReload = dev.PairOnHotReload; + + // If using custom BT Pairing Mode, set the pairing address to the desired one. Otherwise, leave it blank so DsHidMini auto-pairs to current BT host + dshmDeviceData.DeviceSettings.CustomPairingAddress = + (dev.BluetoothPairingMode == BluetoothPairingMode.Custom) ? dev.PairingAddress : null; + + switch (dev.SettingsMode) + { + case SettingsModes.Custom: + DshmManagerToDriverConversion.ConvertDeviceSettingsToDriverFormat(dev.Settings,dshmDeviceData.DeviceSettings); + break; + case SettingsModes.Profile: + ProfileData devprof = GetProfile(dev.GuidOfProfileToUse); + DshmManagerToDriverConversion.ConvertDeviceSettingsToDriverFormat(devprof.Settings,dshmDeviceData.DeviceSettings); + break; + + case SettingsModes.Global: + default: + // Device's in Global settings mode need to have empty settings so global settings are not overwritten + break; + } + dshmConfiguration.Devices.Add(dshmDeviceData); + } + + Log.Logger.Debug("Configuration object built. Applying configuration."); + var updateStatus = dshmConfiguration.ApplyConfiguration(); + DshmConfigurationUpdated?.Invoke(this, new DshmUpdatedEventArgs() { UpdatedSuccessfully = updateStatus}); + } + + public List GetListOfProfilesWithDefault() + { + var userProfilesPlusDefault = new List(dshmManagerUserData.Profiles); + userProfilesPlusDefault.Insert(0, ProfileData.DefaultProfile); + return userProfilesPlusDefault; + } + + /// + /// Adds to the Profile List a new profile with the given name and settings based on the default profile + /// + /// Name of the new profile + /// The created profile + public ProfileData CreateProfile(string profileName) + { + ProfileData newProfile = new(); + newProfile.ProfileName = profileName; + //newProfile.DiskFileName = profileName + ".json"; + dshmManagerUserData.Profiles.Add(newProfile); + Log.Logger.Information($"Profile '{profileName}' created on DsHidMini User Data."); + return newProfile; + } + + /// + /// Removes the given profile from the profile list. + /// Devices set to use it will be updated to use the default profile and will be reverted back to Global settings mode if in Profile settings mode + /// + /// The profile to be deleted + public void DeleteProfile(ProfileData profile) + { + Log.Logger.Information($"Deleting profile '{profile.ProfileName}'"); + if (profile == ProfileData.DefaultProfile) // Never remove Default profile from the list + { + Log.Logger.Information($"Default Profile can't be deleted."); + return; + } + dshmManagerUserData.Profiles.Remove(profile); + FixDevicesWithBlankProfiles(); + } + + public SettingsContext GetDeviceExpectedHidMode(DeviceData dev) + { + switch (dev.SettingsMode) + { + case SettingsModes.Custom: + return dev.Settings.HidMode.SettingsContext; + break; + case SettingsModes.Profile: + return GetProfile(dev.GuidOfProfileToUse).Settings.HidMode.SettingsContext; + break; + case SettingsModes.Global: + default: + return GlobalProfile.Settings.HidMode.SettingsContext; + break; + } + } + + /// + /// Gets the DsHidMini config. manager device data of a DsHidMini device. If it does not exist, a new one will be created for it first before returning + /// + /// The MAC address of the DsHidMini device + /// The device data of the DsHidMini device + public DeviceData GetDeviceData(string deviceMac) + { + Log.Logger.Information($"Getting data for device {deviceMac}."); + foreach (DeviceData dev in dshmManagerUserData.Devices) + { + if (dev.DeviceMac == deviceMac) + { + return dev; + } + } + Log.Logger.Information($"Data for Device {deviceMac} does not exist. Creating new."); + var newDevice = new DeviceData(deviceMac); + newDevice.DeviceMac = deviceMac; + dshmManagerUserData.Devices.Add(newDevice); + return newDevice; + } + + public class DshmUpdatedEventArgs : EventArgs + { + public bool UpdatedSuccessfully; + } + + } +} \ No newline at end of file diff --git a/ControlApp/Models/DshmConfigManager/DshmTranslationUtils.cs b/ControlApp/Models/DshmConfigManager/DshmTranslationUtils.cs new file mode 100644 index 00000000..89ab0893 --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/DshmTranslationUtils.cs @@ -0,0 +1,250 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfig.Enums; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Button = Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums.Button; +using LEDsMode = Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums.LEDsMode; +using PressureMode = Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums.PressureMode; +using static Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.LedsSettings; + +namespace Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager +{ + public class DshmManagerToDriverConversion + { + public static Dictionary HidDeviceMode = new() + { + {SettingsContext.Global , DshmConfig.Enums.HidDeviceMode.XInput}, + {SettingsContext.General , DshmConfig.Enums.HidDeviceMode.XInput}, + {SettingsContext.SDF , DshmConfig.Enums.HidDeviceMode.SDF}, + {SettingsContext.GPJ , DshmConfig.Enums.HidDeviceMode.GPJ}, + {SettingsContext.SXS , DshmConfig.Enums.HidDeviceMode.SXS}, + {SettingsContext.DS4W , DshmConfig.Enums.HidDeviceMode.DS4Windows}, + {SettingsContext.XInput , DshmConfig.Enums.HidDeviceMode.XInput}, + }; + + //---------------------------------------------------- LEDsModes + + public static Dictionary LedModeManagerToDriver = new() + { + { LEDsMode.BatteryIndicatorPlayerIndex, DshmConfig.Enums.LEDsMode.BatteryIndicatorPlayerIndex }, + { LEDsMode.BatteryIndicatorBarGraph, DshmConfig.Enums.LEDsMode.BatteryIndicatorBarGraph }, + { LEDsMode.CustomStatic, DshmConfig.Enums.LEDsMode.CustomPattern }, + { LEDsMode.CustomPattern, DshmConfig.Enums.LEDsMode.CustomPattern }, + }; + + //---------------------------------------------------- DPadModes + + public static Dictionary DPadExposureModeManagerToDriver = new() + { + { DPadMode.Default, DPadExposureMode.Default }, + { DPadMode.HAT, DPadExposureMode.HAT }, + { DPadMode.Buttons, DPadExposureMode.IndividualButtons }, + }; + + //---------------------------------------------------- PressureModes + + public static Dictionary DsPressureModeManagerToDriver = new() + { + { PressureMode.Default, DshmConfig.Enums.PressureMode.Default }, + { PressureMode.Analogue, DshmConfig.Enums.PressureMode.Analogue }, + { PressureMode.Digital, DshmConfig.Enums.PressureMode.Digital }, + }; + + public static Dictionary ButtonManagerToDriver = new() + { + { Button.Select, 0 }, + { Button.L3, 1 }, + { Button.R3, 2 }, + { Button.Start, 3 }, + { Button.Up, 4 }, + { Button.Right, 5 }, + { Button.Down, 6 }, + { Button.Left, 7 }, + { Button.L2, 8 }, + { Button.R2, 9 }, + { Button.L1, 10 }, + { Button.R1, 11 }, + { Button.Triangle, 12 }, + { Button.Circle, 13 }, + { Button.Cross, 14 }, + { Button.Square, 15 }, + { Button.PS, 16 }, + }; + + public static Dictionary PairingModeManagerToDriver = new() + { + { BluetoothPairingMode.Auto, DevicePairingMode.Auto }, + { BluetoothPairingMode.Custom, DevicePairingMode.Custom }, + { BluetoothPairingMode.Disabled, DevicePairingMode.Disabled }, + }; + + public static void ConvertDeviceSettingsToDriverFormat(DeviceSettings appFormat, DshmDeviceSettings driverFormat) + { + //////////////////////////////////////////////////////////////////////////////// + // HID MODE + //////////////////////////////////////////////////////////////////////////////// + var x_HidMode = appFormat.HidMode; + if (appFormat.HidMode.SettingsContext != SettingsContext.General) + { + driverFormat.HIDDeviceMode = DshmManagerToDriverConversion.HidDeviceMode[x_HidMode.SettingsContext]; + driverFormat.ContextSettings.HIDDeviceMode = driverFormat.HIDDeviceMode; + } + + driverFormat.ContextSettings.PressureExposureMode = + (x_HidMode.SettingsContext == SettingsContext.SDF + || x_HidMode.SettingsContext == SettingsContext.GPJ) + ? DshmManagerToDriverConversion.DsPressureModeManagerToDriver[x_HidMode.PressureExposureMode] : null; + + driverFormat.ContextSettings.DPadExposureMode = + (x_HidMode.SettingsContext == SettingsContext.SDF + || x_HidMode.SettingsContext == SettingsContext.GPJ) + ? DshmManagerToDriverConversion.DPadExposureModeManagerToDriver[x_HidMode.DPadExposureMode] : null; + + //////////////////////////////////////////////////////////////////////////////// + // LEDS + //////////////////////////////////////////////////////////////////////////////// + var x_Leds = appFormat.LEDs; + DshmDeviceSettings.AllLEDSettings dshm_AllLEDsSettings = driverFormat.ContextSettings.LEDSettings; + + dshm_AllLEDsSettings.Mode = DshmManagerToDriverConversion.LedModeManagerToDriver[x_Leds.LeDMode]; + dshm_AllLEDsSettings.Authority = x_Leds.AllowExternalLedsControl ? DSHM_LEDsAuthority.Automatic : DSHM_LEDsAuthority.Driver; + + + if (x_Leds.LeDMode == LEDsMode.CustomPattern || x_Leds.LeDMode == LEDsMode.CustomStatic) + { + var dshm_Customs = dshm_AllLEDsSettings.CustomPatterns; + + var dshm_singleLED = new DshmDeviceSettings.SingleLEDCustoms[] + { dshm_Customs.Player1, dshm_Customs.Player2,dshm_Customs.Player3,dshm_Customs.Player4, }; + + dshm_Customs.LEDFlags = 0; + for (int i = 0; i < x_Leds.LEDsCustoms.LED_x_Customs.Length; i++) + { + All4LEDsCustoms.singleLEDCustoms singleLEDCustoms = x_Leds.LEDsCustoms.LED_x_Customs[i]; + + if (singleLEDCustoms.IsLedEnabled) + { + dshm_Customs.LEDFlags |= (byte)(1 << (1 + i)); + dshm_singleLED[i].TotalDuration = (x_Leds.LeDMode == LEDsMode.CustomPattern) ? singleLEDCustoms.Duration : (byte)0xFF; + dshm_singleLED[i].BasePortionDuration = (x_Leds.LeDMode == LEDsMode.CustomPattern) ? (ushort)(singleLEDCustoms.CycleDuration) : (byte)0x01; + dshm_singleLED[i].OffPortionMultiplier = (x_Leds.LeDMode == LEDsMode.CustomPattern) ? singleLEDCustoms.OffPeriodCycles : (byte)0x00; + dshm_singleLED[i].OnPortionMultiplier = (x_Leds.LeDMode == LEDsMode.CustomPattern) ? singleLEDCustoms.OnPeriodCycles : (byte)0x01; + } + else + { + dshm_singleLED[i].TotalDuration = (byte)0x00; + dshm_singleLED[i].BasePortionDuration = (byte)0x00; + dshm_singleLED[i].OffPortionMultiplier = (byte)0x00; + dshm_singleLED[i].OnPortionMultiplier = (byte)0x00; + } + } + if(dshm_Customs.LEDFlags == 0) dshm_Customs.LEDFlags = (byte)0x20; // Turn off all LEDs with 0x20 if none has been enabled + } + //////////////////////////////////////////////////////////////////////////////// + // WIRELESS + //////////////////////////////////////////////////////////////////////////////// + var x_Wireless = appFormat.Wireless; + + driverFormat.DisableWirelessIdleTimeout = !x_Wireless.IsWirelessIdleDisconnectEnabled; + driverFormat.WirelessIdleTimeoutPeriodMs = x_Wireless.WirelessIdleDisconnectTime; + + driverFormat.QuickDisconnectCombo.IsEnabled = x_Wireless.QuickDisconnectCombo.IsEnabled; + driverFormat.QuickDisconnectCombo.HoldTime = x_Wireless.QuickDisconnectCombo.HoldTime; + driverFormat.QuickDisconnectCombo.Button1 = DshmManagerToDriverConversion.ButtonManagerToDriver[x_Wireless.QuickDisconnectCombo.ButtonCombo[0]]; + driverFormat.QuickDisconnectCombo.Button2 = DshmManagerToDriverConversion.ButtonManagerToDriver[x_Wireless.QuickDisconnectCombo.ButtonCombo[1]]; + driverFormat.QuickDisconnectCombo.Button3 = DshmManagerToDriverConversion.ButtonManagerToDriver[x_Wireless.QuickDisconnectCombo.ButtonCombo[2]]; + + //////////////////////////////////////////////////////////////////////////////// + // Sticks + //////////////////////////////////////////////////////////////////////////////// + var x_Sticks = appFormat.Sticks; + DshmDeviceSettings.DeadZoneSettings dshmLeftDZSettings = driverFormat.ContextSettings.DeadZoneLeft; + DshmDeviceSettings.DeadZoneSettings dshmRightDZSettings = driverFormat.ContextSettings.DeadZoneRight; + DshmDeviceSettings.AxesFlipping axesFlipping = driverFormat.ContextSettings.FlipAxis; + + dshmLeftDZSettings.Apply = x_Sticks.LeftStickData.IsDeadZoneEnabled; + dshmLeftDZSettings.PolarValue = (byte)(x_Sticks.LeftStickData.DeadZone * 181 / 142); + axesFlipping.LeftX = x_Sticks.LeftStickData.InvertXAxis; + axesFlipping.LeftY = x_Sticks.LeftStickData.InvertYAxis; + + dshmRightDZSettings.Apply = x_Sticks.RightStickData.IsDeadZoneEnabled; + dshmRightDZSettings.PolarValue = (byte)(x_Sticks.RightStickData.DeadZone * 181 / 142); + axesFlipping.RightX = x_Sticks.RightStickData.InvertXAxis; + axesFlipping.RightY = x_Sticks.RightStickData.InvertYAxis; + + //////////////////////////////////////////////////////////////////////////////// + // General Rumble + //////////////////////////////////////////////////////////////////////////////// + var x_RumbleGeneral = appFormat.GeneralRumble; + DshmDeviceSettings.AllRumbleSettings dshmRumbleSettings = driverFormat.ContextSettings.RumbleSettings; + + dshmRumbleSettings.DisableLeft = x_RumbleGeneral.IsLeftMotorDisabled; + dshmRumbleSettings.DisableRight = x_RumbleGeneral.IsRightMotorDisabled; + + dshmRumbleSettings.AlternativeMode.IsEnabled = x_RumbleGeneral.AlwaysStartInNormalMode ? false : x_RumbleGeneral.IsAltRumbleModeEnabled; + + if (x_RumbleGeneral.IsAltRumbleModeEnabled) + { + dshmRumbleSettings.AlternativeMode.ToggleCombo.IsEnabled = true; + dshmRumbleSettings.AlternativeMode.ToggleCombo.HoldTime = x_RumbleGeneral.AltModeToggleButtonCombo.HoldTime; + dshmRumbleSettings.AlternativeMode.ToggleCombo.Button1 = DshmManagerToDriverConversion.ButtonManagerToDriver[x_RumbleGeneral.AltModeToggleButtonCombo.ButtonCombo[0]]; + dshmRumbleSettings.AlternativeMode.ToggleCombo.Button2 = DshmManagerToDriverConversion.ButtonManagerToDriver[x_RumbleGeneral.AltModeToggleButtonCombo.ButtonCombo[1]]; + dshmRumbleSettings.AlternativeMode.ToggleCombo.Button3 = DshmManagerToDriverConversion.ButtonManagerToDriver[x_RumbleGeneral.AltModeToggleButtonCombo.ButtonCombo[2]]; + + } + else + { + dshmRumbleSettings.AlternativeMode.ToggleCombo.IsEnabled = false; + } + + //////////////////////////////////////////////////////////////////////////////// + // Output Report + //////////////////////////////////////////////////////////////////////////////// + var x_OutRep = appFormat.OutputReport; + + driverFormat.IsOutputRateControlEnabled = x_OutRep.IsOutputReportRateControlEnabled; + driverFormat.OutputRateControlPeriodMs = (byte)x_OutRep.MaxOutputRate; + driverFormat.IsOutputDeduplicatorEnabled = x_OutRep.IsOutputReportDeduplicatorEnabled; + + //////////////////////////////////////////////////////////////////////////////// + /// Left Motor Rescaling + //////////////////////////////////////////////////////////////////////////////// + var x_LeftMRescale = appFormat.LeftMotorRescaling; + DshmDeviceSettings.HeavyRescaleSettings dshmLeftRumbleRescaleSettings = driverFormat.ContextSettings.RumbleSettings.HeavyRescale; + + dshmLeftRumbleRescaleSettings.IsEnabled = x_LeftMRescale.IsLeftMotorStrRescalingEnabled; + dshmLeftRumbleRescaleSettings.RescaleMinRange = (byte)x_LeftMRescale.LeftMotorStrRescalingLowerRange; + dshmLeftRumbleRescaleSettings.RescaleMaxRange = (byte)x_LeftMRescale.LeftMotorStrRescalingUpperRange; + + //////////////////////////////////////////////////////////////////////////////// + // Alt Mode Adjuster + //////////////////////////////////////////////////////////////////////////////// + var x_AltRumbleAdjusts = appFormat.AltRumbleAdjusts; + DshmDeviceSettings.AlternativeModeSettings dshmSMConversionSettings = driverFormat.ContextSettings.RumbleSettings.AlternativeMode; + DshmDeviceSettings.ForcedRightAdjusts dshmForcedSMSettings = driverFormat.ContextSettings.RumbleSettings.AlternativeMode.ForcedRight; + + dshmSMConversionSettings.RescaleMinRange = (byte)x_AltRumbleAdjusts.RightRumbleConversionLowerRange; + dshmSMConversionSettings.RescaleMaxRange = (byte)x_AltRumbleAdjusts.RightRumbleConversionUpperRange; + + // Right rumble (light) threshold + dshmForcedSMSettings.IsLightThresholdEnabled = x_AltRumbleAdjusts.IsForcedRightMotorLightThresholdEnabled; + dshmForcedSMSettings.LightThreshold = (byte)x_AltRumbleAdjusts.ForcedRightMotorLightThreshold; + + // Left rumble (Heavy) threshold + dshmForcedSMSettings.IsHeavyThresholdEnabled = x_AltRumbleAdjusts.IsForcedRightMotorHeavyThreasholdEnabled; + dshmForcedSMSettings.HeavyThreshold = (byte)x_AltRumbleAdjusts.ForcedRightMotorHeavyThreshold; + + //////////////////////////////////////////////////////////////////////////////// + // Fine tweaking + //////////////////////////////////////////////////////////////////////////////// + if (appFormat.HidMode.SettingsContext == Enums.SettingsContext.DS4W) + { + if (appFormat.HidMode.PreventRemappingConflictsInDS4WMode) + { + driverFormat.ContextSettings.DeadZoneLeft.Apply = false; + driverFormat.ContextSettings.DeadZoneRight.Apply = false; + } + } + } + } +} diff --git a/ControlApp/Models/DshmConfigManager/Enums/DshmConfigManagerEnums.cs b/ControlApp/Models/DshmConfigManager/Enums/DshmConfigManagerEnums.cs new file mode 100644 index 00000000..9a6c28cc --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/Enums/DshmConfigManagerEnums.cs @@ -0,0 +1,89 @@ +namespace Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums +{ + public enum SettingsModes + { + Global, + Profile, + Custom, + } + public enum SettingsContext + { + Unknown, + SDF, + GPJ, + SXS, + DS4W, + XInput, + General, + Global, + } + + public enum SettingsModeGroups + { + LEDsControl, + WirelessSettings, + OutputReportControl, + SticksDeadzone, + RumbleGeneral, + RumbleLeftStrRescale, + RumbleRightConversion, + Unique_All, + Unique_Global, + Unique_General, + Unique_SDF, + Unique_GPJ, + Unique_SXS, + Unique_DS4W, + Unique_XInput, + } + + public enum LEDsMode + { + BatteryIndicatorPlayerIndex, + BatteryIndicatorBarGraph, + CustomStatic, + CustomPattern, + } + + public enum Button + { + PS, + Start, + Select, + R1, + L1, + R2, + L2, + R3, + L3, + Triangle, + Circle, + Cross, + Square, + Up, + Right, + Down, + Left, + } + + public enum PressureMode + { + Digital, + Analogue, + Default, + } + + public enum DPadMode + { + Default, + HAT, + Buttons, + } + + public enum BluetoothPairingMode + { + Auto, + Custom, + Disabled, + } +} diff --git a/ControlApp/Models/DshmConfigManager/JsonDshmUserData.cs b/ControlApp/Models/DshmConfigManager/JsonDshmUserData.cs new file mode 100644 index 00000000..80f8da87 --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/JsonDshmUserData.cs @@ -0,0 +1,98 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Rico Suter. All rights reserved. +// +// http://visualjsoneditor.codeplex.com/license +// Rico Suter, mail@rsuter.com +//----------------------------------------------------------------------- + +using System.IO; +using System.Text; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +using Serilog; + +namespace Nefarius.DsHidMini.ControlApp.Models +{ + /// Provides methods to load and save the application configuration. + public static class JsonDshmUserData + { + private const string ConfigExtension = ".json"; + + /// Loads the application configuration. + /// The type of the application configuration. + /// The configuration file name without extension. + /// Defines if the schema file should always be generated and overwritten. + /// Defines the directory the UserData file will be loaded from. + /// The configuration object. + /// An I/O error occurred while opening the file. + public static T Load(string fileNameWithoutExtension, bool alwaysCreateNewSchemaFile, string userDataDir) where T : new() + { + Log.Logger.Debug($"Loading DsHidMini User Data from {fileNameWithoutExtension} file in {userDataDir}."); + var configPath = CreateFilePath(fileNameWithoutExtension, ConfigExtension, userDataDir); + + if (!File.Exists(configPath)) + { + Log.Logger.Debug($"User Data file does not exist in the specified directory. Creating new User Data."); + return CreateDefaultConfigurationFile(fileNameWithoutExtension, userDataDir); + } + + // Handle errors on deserialization + var settings = new JsonSerializerSettings() { Error = HandleDeserializationError, }; + return JsonConvert.DeserializeObject(File.ReadAllText(configPath, Encoding.UTF8), settings); + } + + /// Saves the configuration. + /// The configuration file name without extension. + /// The configuration object to store. + /// Defines the directory the UserData file will be stored. + /// An I/O error occurred while opening the file. + public static void Save(string fileNameWithoutExtension, T configuration, string userDataDir) where T : new() + { + Log.Logger.Debug($"Saving DsHidMini User Data to {fileNameWithoutExtension} in dir {userDataDir}"); + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new StringEnumConverter()); + + var configPath = CreateFilePath(fileNameWithoutExtension, ConfigExtension, userDataDir); + File.WriteAllText(configPath, JsonConvert.SerializeObject(configuration, Formatting.Indented, settings), Encoding.UTF8); + } + + private static void HandleDeserializationError(object? sender, Newtonsoft.Json.Serialization.ErrorEventArgs errorArgs) + { + var currentError = errorArgs.ErrorContext.Error.Message; + errorArgs.ErrorContext.Handled = true; + } + + private static string CreateFilePath(string fileNameWithoutExtension, string extension, string? userDataDir) + { + if (userDataDir != null) + { + var filePath = Path.Combine(userDataDir, fileNameWithoutExtension) + extension; + + var directoryPath = Path.GetDirectoryName(filePath); + if (directoryPath != null && !Directory.Exists(directoryPath)) + { + Log.Logger.Debug("Specified directory of DsHidMini User Data does not exist. Creating directory."); + Directory.CreateDirectory(directoryPath); + } + + + return filePath; + } + return fileNameWithoutExtension + extension; + } + + private static T CreateDefaultConfigurationFile(string fileNameWithoutExtension, string userDataDir) where T : new() + { + Log.Logger.Debug("Creating default configuration file for DsHidMini User Data."); + var config = new T(); + var configData = JsonConvert.SerializeObject(config, Formatting.Indented); + var configPath = CreateFilePath(fileNameWithoutExtension, ConfigExtension, userDataDir); + + File.WriteAllText(configPath, configData, Encoding.UTF8); + return config; + } + } +} \ No newline at end of file diff --git a/ControlApp/Models/DshmConfigManager/ProfileData.cs b/ControlApp/Models/DshmConfigManager/ProfileData.cs new file mode 100644 index 00000000..810c5ca9 --- /dev/null +++ b/ControlApp/Models/DshmConfigManager/ProfileData.cs @@ -0,0 +1,27 @@ +namespace Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager +{ + public class ProfileData + { + public static readonly Guid DefaultGuid = new Guid("00000000000000000000000000000000"); + public string ProfileName { get; set; } + public Guid ProfileGuid { get; set; } = Guid.NewGuid(); + + public DeviceSettings Settings { get; set; } = new(); + + public ProfileData() + { + } + + public static readonly ProfileData DefaultProfile = new() + { + ProfileName = "XInput (Default)", + ProfileGuid = DefaultGuid, + Settings = new(), + }; + + public override string ToString() + { + return ProfileName; + } + } +} \ No newline at end of file diff --git a/ControlApp/Models/DshmDevMan.cs b/ControlApp/Models/DshmDevMan.cs new file mode 100644 index 00000000..b5b00173 --- /dev/null +++ b/ControlApp/Models/DshmDevMan.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Nefarius.DsHidMini.ControlApp.Models.Drivers; +using Nefarius.DsHidMini.ControlApp.Models.Util; +using Nefarius.Utilities.DeviceManagement.PnP; +using Nefarius.Utilities.Bluetooth; +using Nefarius.DsHidMini.ControlApp.Services; +using Nefarius.DsHidMini.ControlApp.ViewModels; +using Nefarius.Utilities.DeviceManagement.Extensions; +using Serilog; + +namespace Nefarius.DsHidMini.ControlApp.Models +{ + public class DshmDevMan + { + private DeviceNotificationListener _listener; + //private readonly HostRadio _hostRadio; + + public List Devices { get; private set; } = new(); + + public DshmDevMan() + { + } + + public bool StartListeningForDshmDevices() + { + Log.Logger.Information("Starting detection of DsHidMini devices"); + if (_listener != null) return false; + _listener = new DeviceNotificationListener(); + _listener.DeviceArrived += OnListenerDevicesRemovedOrAdded; + _listener.DeviceRemoved += OnListenerDevicesRemovedOrAdded; + _listener.StartListen(DsHidMiniDriver.DeviceInterfaceGuid); + + UpdateConnectedDshmDevicesList(); + return true; + } + + public void StopListeningForDshmDevices() + { + Log.Logger.Information("Stopping detection of DsHidMini devices"); + Devices.Clear(); + _listener.StopListen(); + _listener.Dispose(); + _listener = null; + } + + public void OnListenerDevicesRemovedOrAdded(DeviceEventArgs e) + { + Log.Logger.Information("DsHidMini devices added or removed. Updating device list"); + UpdateConnectedDshmDevicesList(); + } + + public void UpdateConnectedDshmDevicesList() + { + Log.Logger.Debug("Rebuilding list of connected DsHidMini devices"); + Devices.Clear(); + var instance = 0; + while (Devcon.FindByInterfaceGuid(DsHidMiniDriver.DeviceInterfaceGuid, out var path, out var instanceId, instance++)) + { + Log.Logger.Debug($"DsHidMini device detected and added to devices list. InstanceID: {instanceId}"); + Devices.Add(PnPDevice.GetDeviceByInstanceId(instanceId)); + } + Log.Logger.Debug($"DsHidMini devices list rebuilt. {Devices.Count} connected devices"); + ConnectedDeviceListUpdated?.Invoke(this, new()); + } + + + public bool TryReconnectDevice(PnPDevice device) + { + Log.Logger.Information($"Attempting on reconnecting device of instance {device.InstanceId}"); + var enumerator = device.GetProperty(DevicePropertyKey.Device_EnumeratorName); + var IsWireless = !enumerator.Equals("USB", StringComparison.InvariantCultureIgnoreCase); + Log.Logger.Debug($"Is Device connected wirelessly: {IsWireless}"); + + if (IsWireless) + { + try + { + HostRadio hostRadio = new(); + var deviceAddress = device.GetProperty(DsHidMiniDriver.DeviceAddressProperty).ToUpper(); + Log.Logger.Debug($"Instructing BT host on disconnecting device of MAC {deviceAddress}"); + hostRadio.DisconnectRemoteDevice(deviceAddress); + return true; + } + catch(Exception ex) + { + Log.Error(ex, "Failed to disconnect wireless device."); + return false; + } + } + else + { + try + { + var ohmy = device.ToUsbPnPDevice(); + Log.Logger.Debug($"Power cycling device's USB port"); + ohmy.CyclePort(); + return true; + } + catch (Exception e) + { + Log.Logger.Error(e,$"Failed to power cycle device's USB port"); + return false; + } + } + } + + public event EventHandler ConnectedDeviceListUpdated; + + } +} diff --git a/ControlApp/Models/Enums/AppEnums.cs b/ControlApp/Models/Enums/AppEnums.cs new file mode 100644 index 00000000..9f726a3b --- /dev/null +++ b/ControlApp/Models/Enums/AppEnums.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Nefarius.DsHidMini.ControlApp.Models.Enums +{ + public enum HidModeShort : byte + { + Unkown = 0x00, + SDF = 0x01, + GPJ = 0x02, + SXS = 0x03, + DS4W = 0x04, + XInput = 0x05, + } +} diff --git a/ControlApp/Models/JsonApplicationConfiguration.cs b/ControlApp/Models/JsonApplicationConfiguration.cs new file mode 100644 index 00000000..933a9e38 --- /dev/null +++ b/ControlApp/Models/JsonApplicationConfiguration.cs @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Rico Suter. All rights reserved. +// +// http://visualjsoneditor.codeplex.com/license +// Rico Suter, mail@rsuter.com +//----------------------------------------------------------------------- + +using System.IO; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Nefarius.DsHidMini.ControlApp.Models +{ + /// Provides methods to load and save the application configuration. + public static class JsonApplicationConfiguration + { + private const string ConfigExtension = ".json"; + + /// Loads the application configuration. + /// The type of the application configuration. + /// The configuration file name without extension. + /// Defines if the schema file should always be generated and overwritten. + /// Defines if the configuration file should be loaded from the user's AppData directory. + /// The configuration object. + /// An I/O error occurred while opening the file. + public static T Load(string fileNameWithoutExtension, bool alwaysCreateNewSchemaFile, bool storeInAppData) where T : new() + { + var configPath = CreateFilePath(fileNameWithoutExtension, ConfigExtension, storeInAppData); + + if (!File.Exists(configPath)) + return CreateDefaultConfigurationFile(fileNameWithoutExtension, storeInAppData); + + return JsonConvert.DeserializeObject(File.ReadAllText(configPath, Encoding.UTF8)); + } + + /// Saves the configuration. + /// The configuration file name without extension. + /// The configuration object to store. + /// Defines if the configuration file should be stored in the user's AppData directory. + /// An I/O error occurred while opening the file. + public static void Save(string fileNameWithoutExtension, T configuration, bool storeInAppData) where T : new() + { + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new StringEnumConverter()); + + var configPath = CreateFilePath(fileNameWithoutExtension, ConfigExtension, storeInAppData); + File.WriteAllText(configPath, JsonConvert.SerializeObject(configuration, Formatting.Indented, settings), Encoding.UTF8); + } + + private static string CreateFilePath(string fileNameWithoutExtension, string extension, bool storeInAppData) + { + if (storeInAppData) + { + var appDataDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var filePath = Path.Combine(appDataDirectory, fileNameWithoutExtension) + extension; + + var directoryPath = Path.GetDirectoryName(filePath); + if (directoryPath != null && !Directory.Exists(directoryPath)) + Directory.CreateDirectory(directoryPath); + + return filePath; + } + return fileNameWithoutExtension + extension; + } + + private static T CreateDefaultConfigurationFile(string fileNameWithoutExtension, bool storeInAppData) where T : new() + { + var config = new T(); + var configData = JsonConvert.SerializeObject(config, Formatting.Indented); + var configPath = CreateFilePath(fileNameWithoutExtension, ConfigExtension, storeInAppData); + + File.WriteAllText(configPath, configData, Encoding.UTF8); + return config; + } + } +} \ No newline at end of file diff --git a/ControlApp/Models/Main.cs b/ControlApp/Models/Main.cs new file mode 100644 index 00000000..b198cc9e --- /dev/null +++ b/ControlApp/Models/Main.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +namespace Nefarius.DsHidMini.ControlApp.Models; +public class Main +{ + public static bool IsAdministrator() + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + + public static void StartAsAdmin(string fileName) + { + var proc = new Process + { + StartInfo = + { + FileName = fileName, + UseShellExecute = true, + Verb = "runas" + } + }; + + proc.Start(); + } + + public static void RestartAsAdmin() + { + if (!IsAdministrator()) + { + Console.WriteLine("restarting as admin"); + StartAsAdmin(Assembly.GetExecutingAssembly().GetName().Name); + App.Current.Shutdown(); + return; + } + } +} diff --git a/ControlApp/Models/Util/BluetoothHelper.cs b/ControlApp/Models/Util/BluetoothHelper.cs new file mode 100644 index 00000000..a37cd6af --- /dev/null +++ b/ControlApp/Models/Util/BluetoothHelper.cs @@ -0,0 +1,72 @@ +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Devices.Bluetooth; + +namespace Nefarius.DsHidMini.ControlApp.Models.Util +{ + public static class BluetoothHelper + { + private const uint IOCTL_BTH_DISCONNECT_DEVICE = 0x41000C; + + public static bool IsBluetoothRadioAvailable + { + get + { + BLUETOOTH_FIND_RADIO_PARAMS radioParams; + radioParams.dwSize = (uint)Marshal.SizeOf(); + + var findHandle = PInvoke.BluetoothFindFirstRadio(radioParams, out var radioHandle); + + if (findHandle == 0) return false; + + PInvoke.BluetoothFindRadioClose(findHandle); + radioHandle.Dispose(); + + return true; + } + } + + /// + /// Instruct host radio to disconnect a given remote device. + /// + /// The MAC address of the remote device. + public static unsafe void DisconnectRemoteDevice(PhysicalAddress device) + { + BLUETOOTH_FIND_RADIO_PARAMS radioParams; + radioParams.dwSize = (uint)Marshal.SizeOf(); + + var findHandle = PInvoke.BluetoothFindFirstRadio(radioParams, out var radioHandle); + + if (findHandle == 0) + return; + + var payloadSize = Marshal.SizeOf(); + var payload = Marshal.AllocHGlobal(payloadSize); + var raw = new byte[] { 0x00, 0x00 }.Concat(device.GetAddressBytes()).Reverse().ToArray(); + var value = (long)BitConverter.ToUInt64(raw, 0); + + Marshal.WriteInt64(payload, value); + + try + { + PInvoke.DeviceIoControl( + radioHandle, + IOCTL_BTH_DISCONNECT_DEVICE, + payload.ToPointer(), + (uint)payloadSize, + null, + 0, + null, + null + ); + } + finally + { + Marshal.FreeHGlobal(payload); + PInvoke.BluetoothFindRadioClose(findHandle); + radioHandle.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/ControlApp/Models/Util/DshmDriverTranslationUtils.cs b/ControlApp/Models/Util/DshmDriverTranslationUtils.cs new file mode 100644 index 00000000..12860596 --- /dev/null +++ b/ControlApp/Models/Util/DshmDriverTranslationUtils.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.Models.Util; +internal class DshmDriverTranslationUtils +{ + public static Dictionary HidDeviceMode = new() + { + { 0x01 , SettingsContext.SDF}, + { 0x02 , SettingsContext.GPJ}, + { 0x03 , SettingsContext.SXS}, + { 0x04 , SettingsContext.DS4W}, + { 0x05 , SettingsContext.XInput}, + }; +} diff --git a/ControlApp/Models/Util/RegistryHelpers.cs b/ControlApp/Models/Util/RegistryHelpers.cs new file mode 100644 index 00000000..7c25297d --- /dev/null +++ b/ControlApp/Models/Util/RegistryHelpers.cs @@ -0,0 +1,25 @@ +using Microsoft.Win32; + +namespace Nefarius.DsHidMini.ControlApp.Models.Util +{ + public static class RegistryHelpers + { + public static RegistryKey GetRegistryKey() + { + return GetRegistryKey(null); + } + + public static RegistryKey GetRegistryKey(string keyPath, bool writable = false) + { + RegistryKey localMachineRegistry + = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, + Environment.Is64BitOperatingSystem + ? RegistryView.Registry64 + : RegistryView.Registry32); + + return string.IsNullOrEmpty(keyPath) + ? localMachineRegistry + : localMachineRegistry.OpenSubKey(keyPath, writable); + } + } +} diff --git a/ControlApp/Models/Util/SecurityUtil.cs b/ControlApp/Models/Util/SecurityUtil.cs new file mode 100644 index 00000000..1b57b04d --- /dev/null +++ b/ControlApp/Models/Util/SecurityUtil.cs @@ -0,0 +1,17 @@ +using System.Security.Principal; + +namespace Nefarius.DsHidMini.ControlApp.Models.Util +{ + public static class SecurityUtil + { + public static bool IsElevated + { + get + { + var securityIdentifier = WindowsIdentity.GetCurrent().Owner; + return !(securityIdentifier is null) && securityIdentifier + .IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid); + } + } + } +} diff --git a/ControlApp/Models/Util/SetupApiWrapper.cs b/ControlApp/Models/Util/SetupApiWrapper.cs new file mode 100644 index 00000000..4df039e2 --- /dev/null +++ b/ControlApp/Models/Util/SetupApiWrapper.cs @@ -0,0 +1,517 @@ +using System.Runtime.InteropServices; + +namespace Nefarius.DsHidMini.ControlApp.Models.Util +{ + internal static class SetupApiWrapper + { + #region Constant and Structure Definitions + + internal const int DIGCF_PRESENT = 0x0002; + internal const int DIGCF_DEVICEINTERFACE = 0x0010; + + internal const int DICD_GENERATE_ID = 0x0001; + internal const int SPDRP_HARDWAREID = 0x0001; + + internal const int DIF_REMOVE = 0x0005; + internal const int DIF_REGISTERDEVICE = 0x0019; + + internal const int DI_REMOVEDEVICE_GLOBAL = 0x0001; + + internal const int DI_NEEDRESTART = 0x00000080; + internal const int DI_NEEDREBOOT = 0x00000100; + + internal const uint DIF_PROPERTYCHANGE = 0x12; + internal const uint DICS_ENABLE = 1; + internal const uint DICS_DISABLE = 2; // disable device + internal const uint DICS_FLAG_GLOBAL = 1; // not profile-specific + internal const uint DIGCF_ALLCLASSES = 4; + internal const uint ERROR_INVALID_DATA = 13; + internal const uint ERROR_NO_MORE_ITEMS = 259; + internal const uint ERROR_ELEMENT_NOT_FOUND = 1168; + + [StructLayout(LayoutKind.Sequential)] + internal struct SP_DEVINFO_DATA + { + internal int cbSize; + internal readonly Guid ClassGuid; + internal readonly uint DevInst; + internal readonly IntPtr Reserved; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SP_CLASSINSTALL_HEADER + { + internal int cbSize; + internal int InstallFunction; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SP_PROPCHANGE_PARAMS + { + internal SP_CLASSINSTALL_HEADER ClassInstallHeader; + internal UInt32 StateChange; + internal UInt32 Scope; + internal UInt32 HwProfile; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SP_REMOVEDEVICE_PARAMS + { + internal SP_CLASSINSTALL_HEADER ClassInstallHeader; + internal int Scope; + internal int HwProfile; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DevPropKey + { + public Guid fmtid; + public uint pid; + + public DevPropKey(Guid fmtid, uint pid) + { + this.fmtid = fmtid; + this.pid = pid; + } + + public DevPropKey(uint a, ushort b, ushort c, byte d, byte e, byte f, byte g, byte h, byte i, byte j, byte k, uint pid) + { + this.fmtid = new Guid(a, b, c, d, e, f, g, h, i, j, k); + this.pid = pid; + } + } + + internal const uint CM_REENUMERATE_NORMAL = 0x00000000; + + internal const uint CM_REENUMERATE_SYNCHRONOUS = 0x00000001; + + // XP and later versions + internal const uint CM_REENUMERATE_RETRY_INSTALLATION = 0x00000002; + + internal const uint CM_REENUMERATE_ASYNCHRONOUS = 0x00000004; + + internal enum CM_QUERY_AND_REMOVE_SUBTREE_FLAGS : uint + { + CM_REMOVE_UI_OK = 0x00000000, + CM_REMOVE_UI_NOT_OK = 0x00000001, + CM_REMOVE_NO_RESTART = 0x00000002, + CM_REMOVE_BITS = 0x00000003 + } + + internal enum CM_SETUP_DEVINST_FLAGS : uint + { + CM_SETUP_DEVNODE_READY = 0x00000000, // Reenable problem devinst + CM_SETUP_DEVINST_READY = CM_SETUP_DEVNODE_READY, + CM_SETUP_DOWNLOAD = 0x00000001, // Get info about devinst + CM_SETUP_WRITE_LOG_CONFS = 0x00000002, + CM_SETUP_PROP_CHANGE = 0x00000003 + } + + internal enum CM_LOCATE_DEVNODE_FLAG : uint + { + CM_LOCATE_DEVNODE_NORMAL = 0x00000000, + CM_LOCATE_DEVNODE_PHANTOM = 0x00000001, + CM_LOCATE_DEVNODE_CANCELREMOVE = 0x00000002, + CM_LOCATE_DEVNODE_NOVALIDATION = 0x00000004, + CM_LOCATE_DEVNODE_BITS = 0x00000007, + } + + internal enum ConfigManagerResult : uint + { + Success = 0x00000000, + Default = 0x00000001, + OutOfMemory = 0x00000002, + InvalidPointer = 0x00000003, + InvalidFlag = 0x00000004, + InvalidDevnode = 0x00000005, + InvalidDevinst = InvalidDevnode, + InvalidResDes = 0x00000006, + InvalidLogConf = 0x00000007, + InvalidArbitrator = 0x00000008, + InvalidNodelist = 0x00000009, + DevnodeHasReqs = 0x0000000A, + DevinstHasReqs = DevnodeHasReqs, + InvalidResourceid = 0x0000000B, + NoSuchDevnode = 0x0000000D, + NoSuchDevinst = NoSuchDevnode, + NoMoreLogConf = 0x0000000E, + NoMoreResDes = 0x0000000F, + AlreadySuchDevnode = 0x00000010, + AlreadySuchDevinst = AlreadySuchDevnode, + InvalidRangeList = 0x00000011, + InvalidRange = 0x00000012, + Failure = 0x00000013, + NoSuchLogicalDev = 0x00000014, + CreateBlocked = 0x00000015, + RemoveVetoed = 0x00000017, + ApmVetoed = 0x00000018, + InvalidLoadType = 0x00000019, + BufferSmall = 0x0000001A, + NoArbitrator = 0x0000001B, + NoRegistryHandle = 0x0000001C, + RegistryError = 0x0000001D, + InvalidDeviceId = 0x0000001E, + InvalidData = 0x0000001F, + InvalidApi = 0x00000020, + DevloaderNotReady = 0x00000021, + NeedRestart = 0x00000022, + NoMoreHwProfiles = 0x00000023, + DeviceNotThere = 0x00000024, + NoSuchValue = 0x00000025, + WrongType = 0x00000026, + InvalidPriority = 0x00000027, + NotDisableable = 0x00000028, + FreeResources = 0x00000029, + QueryVetoed = 0x0000002A, + CantShareIrq = 0x0000002B, + NoDependent = 0x0000002C, + SameResources = 0x0000002D, + NoSuchRegistryKey = 0x0000002E, + InvalidMachinename = 0x0000002F, // NT ONLY + RemoteCommFailure = 0x00000030, // NT ONLY + MachineUnavailable = 0x00000031, // NT ONLY + NoCmServices = 0x00000032, // NT ONLY + AccessDenied = 0x00000033, // NT ONLY + CallNotImplemented = 0x00000034, + InvalidProperty = 0x00000035, + DeviceInterfaceActive = 0x00000036, + NoSuchDeviceInterface = 0x00000037, + InvalidReferenceString = 0x00000038, + InvalidConflictList = 0x00000039, + InvalidIndex = 0x0000003A, + InvalidStructureSize = 0x0000003B + } + + internal const int DEVPROP_TYPEMOD_ARRAY = 0x00001000; // array of fixed-sized data elements + internal const int DEVPROP_TYPEMOD_LIST = 0x00002000; // list of variable-sized data elements + + internal enum DevPropType : uint + { + TYPEMOD_ARRAY = 0x00001000, // array of fixed-sized data elements + TYPEMOD_LIST = 0x00002000, // list of variable-sized data elements + + // + // Property data types. + // + Empty = 0x00000000, // nothing, no property data + Null = 0x00000001, // null property data + Sbyte = 0x00000002, // 8-bit signed int (sbyte) + Byte = 0x00000003, // 8-bit unsigned int (byte) + Int16 = 0x00000004, // 16-bit signed int (short) + Uint16 = 0x00000005, // 16-bit unsigned int (ushort) + Int32 = 0x00000006, // 32-bit signed int (long) + Uint32 = 0x00000007, // 32-bit unsigned int (ulong) + Int64 = 0x00000008, // 64-bit signed int (long64) + Uint64 = 0x00000009, // 64-bit unsigned int (ulong64) + Float = 0x0000000a, // 32-bit floating-point (float) + Double = 0x0000000b, // 64-bit floating-point (double) + Decimal = 0x0000000c, // 128-bit data (decimal) + Guid = 0x0000000d, // 128-bit unique identifier (guid) + Currency = 0x0000000e, // 64 bit signed int currency value (currency) + Date = 0x0000000f, // date (date) + FileTime = 0x00000010, // file time (filetime) + Boolean = 0x00000011, // 8-bit boolean (devprop_boolean) + String = 0x00000012, // null-terminated string + StringList = (String | TYPEMOD_LIST), // multi-sz string list + SecurityDescriptor = 0x00000013, // self-relative binary security_descriptor + SecurityDescriptorString = 0x00000014, // security descriptor string (sddl format) + Devpropkey = 0x00000015, // device property key (devpropkey) + Devproptype = 0x00000016, // device property type (devproptype) + Binary = (Byte | TYPEMOD_ARRAY), // custom binary data + Error = 0x00000017, // 32-bit win32 system error code + Ntstatus = 0x00000018, // 32-bit ntstatus code + StringIndirect = 0x00000019, // string resource (@[path\],-) + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SP_DRVINFO_DATA + { + internal readonly uint cbSize; + internal readonly uint DriverType; + internal readonly IntPtr Reserved; + internal readonly string Description; + internal readonly string MfgName; + internal readonly string ProviderName; + internal readonly DateTime DriverDate; + internal readonly ulong DriverVersion; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SP_DEVICE_INTERFACE_DATA + { + internal Int32 cbSize; + internal Guid interfaceClassGuid; + internal Int32 flags; + internal UIntPtr reserved; + } + + + [Flags] + internal enum DiFlags : uint + { + DIIDFLAG_SHOWSEARCHUI = 1, + DIIDFLAG_NOFINISHINSTALLUI = 2, + DIIDFLAG_INSTALLNULLDRIVER = 3 + } + + internal const uint DIIRFLAG_FORCE_INF = 0x00000002; + + internal const uint INSTALLFLAG_FORCE = 0x00000001; // Force the installation of the specified driver + internal const uint INSTALLFLAG_READONLY = 0x00000002; // Do a read-only install (no file copy) + internal const uint INSTALLFLAG_NONINTERACTIVE = 0x00000004; + + internal enum PNP_VETO_TYPE : uint + { + PNP_VetoTypeUnknown, // Name is unspecified + PNP_VetoLegacyDevice, // Name is an Instance Path + PNP_VetoPendingClose, // Name is an Instance Path + PNP_VetoWindowsApp, // Name is a Module + PNP_VetoWindowsService, // Name is a Service + PNP_VetoOutstandingOpen, // Name is an Instance Path + PNP_VetoDevice, // Name is an Instance Path + PNP_VetoDriver, // Name is a Driver Service Name + PNP_VetoIllegalDeviceRequest, // Name is an Instance Path + PNP_VetoInsufficientPower, // Name is unspecified + PNP_VetoNonDisableable, // Name is an Instance Path + PNP_VetoLegacyDriver, // Name is a Service + PNP_VetoInsufficientRights // Name is unspecified + } + + #endregion + + #region Interop Definitions + + #region SetupAPI + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern IntPtr SetupDiCreateDeviceInfoList(ref Guid ClassGuid, IntPtr hwndParent); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool SetupDiCreateDeviceInfo(IntPtr DeviceInfoSet, string DeviceName, ref Guid ClassGuid, + string DeviceDescription, IntPtr hwndParent, int CreationFlags, ref SP_DEVINFO_DATA DeviceInfoData); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool SetupDiSetDeviceRegistryProperty(IntPtr DeviceInfoSet, + ref SP_DEVINFO_DATA DeviceInfoData, int Property, [MarshalAs(UnmanagedType.LPWStr)] string PropertyBuffer, + int PropertyBufferSize); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool SetupDiCallClassInstaller(int InstallFunction, IntPtr DeviceInfoSet, + ref SP_DEVINFO_DATA DeviceInfoData); + + [DllImport("setupapi.dll", SetLastError = true)] + internal static extern bool SetupDiEnumDeviceInfo(IntPtr deviceInfoSet, + UInt32 memberIndex, + [Out] out SP_DEVINFO_DATA deviceInfoData); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, IntPtr Enumerator, IntPtr hwndParent, + int Flags); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool SetupDiEnumDeviceInterfaces(IntPtr DeviceInfoSet, IntPtr DeviceInfoData, + ref Guid InterfaceClassGuid, int MemberIndex, ref SP_DEVINFO_DATA DeviceInterfaceData); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr DeviceInfoSet, + ref SP_DEVINFO_DATA DeviceInterfaceData, IntPtr DeviceInterfaceDetailData, + int DeviceInterfaceDetailDataSize, + ref int RequiredSize, ref SP_DEVINFO_DATA DeviceInfoData); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool SetupDiOpenDeviceInfo(IntPtr DeviceInfoSet, string DeviceInstanceId, + IntPtr hwndParent, int Flags, ref SP_DEVINFO_DATA DeviceInfoData); + + [DllImport("setupapi.dll", SetLastError = true)] + internal static extern bool SetupDiChangeState( + IntPtr deviceInfoSet, + [In] ref SP_DEVINFO_DATA deviceInfoData); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool SetupDiSetClassInstallParams(IntPtr DeviceInfoSet, + ref SP_DEVINFO_DATA DeviceInterfaceData, ref SP_REMOVEDEVICE_PARAMS ClassInstallParams, + int ClassInstallParamsSize); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool SetupDiGetDeviceInstallParams( + IntPtr hDevInfo, + ref SP_DEVINFO_DATA DeviceInfoData, + IntPtr DeviceInstallParams + ); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool SetupDiGetDeviceProperty( + IntPtr deviceInfoSet, + [In] ref SP_DEVINFO_DATA DeviceInfoData, + [In] ref DevPropKey propertyKey, + [Out] out UInt32 propertyType, + IntPtr propertyBuffer, + UInt32 propertyBufferSize, + out UInt32 requiredSize, + UInt32 flags); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool SetupDiGetDeviceRegistryProperty( + IntPtr DeviceInfoSet, + [In] ref SP_DEVINFO_DATA DeviceInfoData, + UInt32 Property, + [Out] out UInt32 PropertyRegDataType, + IntPtr PropertyBuffer, + UInt32 PropertyBufferSize, + [In,Out] ref UInt32 RequiredSize + ); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool SetupDiRestartDevices( + IntPtr DeviceInfoSet, + [In] ref SP_DEVINFO_DATA DeviceInfoData + ); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool SetupDiOpenDeviceInterface( + [Out] IntPtr DeviceInfoSet, + [In] string DevicePath, + [In] uint OpenFlags, + [In] [Out] ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData + ); + + #endregion + + #region Cfgmgr32 + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Get_Device_ID( + uint DevInst, + IntPtr Buffer, + uint BufferLen, + uint Flags + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Locate_DevNode( + ref uint pdnDevInst, + string pDeviceID, + CM_LOCATE_DEVNODE_FLAG ulFlags + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Locate_DevNode_Ex( + out uint pdnDevInst, + IntPtr pDeviceID, + uint ulFlags, + IntPtr hMachine + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Reenumerate_DevNode_Ex( + uint dnDevInst, + uint ulFlags, + IntPtr hMachine + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Get_Device_ID_Size( + ref uint pulLen, + uint dnDevInst, + uint ulFlags + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Get_DevNode_Property_Keys( + uint devInst, + [Out] DevPropKey[] propertyKeyArray, + ref uint propertyKeyCount, + uint zeroFlags + ); + + [DllImport("CfgMgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Get_DevNode_Property( + uint devInst, + ref DevPropKey propertyKey, + out DevPropType propertyType, + IntPtr buffer, + ref uint bufferSize, + uint flags + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Set_DevNode_Property( + uint devInst, + ref DevPropKey PropertyKey, + DevPropType PropertyType, + IntPtr PropertyBuffer, + uint PropertyBufferSize, + uint ulFlags // reserved + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Get_Device_Interface_Property( + string pszDeviceInterface, + ref DevPropKey PropertyKey, + out DevPropType PropertyType, + IntPtr PropertyBuffer, + ref uint PropertyBufferSize, + uint ulFlags // reserved + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Query_And_Remove_SubTree( + uint dnAncestor, + ref PNP_VETO_TYPE pVetoType, + string pszVetoName, + uint ulNameLength, + CM_QUERY_AND_REMOVE_SUBTREE_FLAGS ulFlags + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Query_And_Remove_SubTree( + uint dnAncestor, + IntPtr pVetoType, + IntPtr pszVetoName, + uint ulNameLength, + CM_QUERY_AND_REMOVE_SUBTREE_FLAGS ulFlags + ); + + [DllImport("Cfgmgr32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern ConfigManagerResult CM_Setup_DevNode( + uint dnDevInst, + CM_SETUP_DEVINST_FLAGS ulFlags + ); + + #endregion + + #region Newdev + + [DllImport("newdev.dll", SetLastError = true)] + internal static extern bool DiInstallDevice( + IntPtr hParent, + IntPtr lpInfoSet, + ref SP_DEVINFO_DATA DeviceInfoData, + ref SP_DRVINFO_DATA DriverInfoData, + DiFlags Flags, + ref bool NeedReboot); + + [DllImport("newdev.dll", SetLastError = true)] + internal static extern bool DiInstallDriver( + IntPtr hwndParent, + string FullInfPath, + uint Flags, + out bool NeedReboot); + + [DllImport("newdev.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool UpdateDriverForPlugAndPlayDevices( + [In, Optional] IntPtr hwndParent, + [In] string HardwareId, + [In] string FullInfPath, + [In] uint InstallFlags, + [Out] out bool bRebootRequired + ); + + #endregion + + #endregion + } +} diff --git a/ControlApp/Models/Util/Web/Validator.cs b/ControlApp/Models/Util/Web/Validator.cs new file mode 100644 index 00000000..076fb6f4 --- /dev/null +++ b/ControlApp/Models/Util/Web/Validator.cs @@ -0,0 +1,75 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.NetworkInformation; + +using Newtonsoft.Json; + +namespace Nefarius.DsHidMini.ControlApp.Models.Util.Web; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class OUIEntry : IEquatable +{ + public OUIEntry(string manufacturer) + { + string hex = manufacturer.Replace(":", string.Empty); + + Bytes = Enumerable.Range(0, hex.Length / 2).Select(x => Convert.ToByte(hex.Substring(x * 2, 2), 16)) + .ToArray(); + } + + public OUIEntry(PhysicalAddress address) + { + Bytes = address.GetAddressBytes().Take(3).ToArray(); + } + + public byte[] Bytes { get; } + + public bool Equals(OUIEntry other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Bytes.SequenceEqual(other.Bytes); + } + + public override bool Equals(object obj) + { + if (obj is OUIEntry entry) + { + return Bytes.SequenceEqual(entry.Bytes); + } + + return false; + } + + public override int GetHashCode() + { + return Bytes.GetHashCode(); + } +} + +public class Validator +{ + public static Uri ApiUrl => new("https://docs.nefarius.at/projects/DsHidMini/genuine_oui_db.json"); + + [Obsolete("Redesign to use modern HttpClient instead.")] + public static bool IsGenuineAddress(PhysicalAddress address) + { + using (WebClient client = new WebClient()) + { + IEnumerable ouiList = JsonConvert.DeserializeObject>(client.DownloadString(ApiUrl)) + .Select(e => new OUIEntry(e)); + + OUIEntry device = new OUIEntry(address); + + return ouiList.Contains(device); + } + } +} \ No newline at end of file diff --git a/ControlApp/NativeMethods.txt b/ControlApp/NativeMethods.txt new file mode 100644 index 00000000..1e48f2ea --- /dev/null +++ b/ControlApp/NativeMethods.txt @@ -0,0 +1,6 @@ +BluetoothFindFirstRadio +BluetoothFindRadioClose +BLUETOOTH_FIND_RADIO_PARAMS +CreateFile +DeviceIoControl +WIN32_ERROR \ No newline at end of file diff --git a/ControlApp/Properties/PublishProfiles/release-win-x64.pubxml b/ControlApp/Properties/PublishProfiles/release-win-x64.pubxml new file mode 100644 index 00000000..0f9a88a4 --- /dev/null +++ b/ControlApp/Properties/PublishProfiles/release-win-x64.pubxml @@ -0,0 +1,20 @@ + + + + + Release + x64 + ..\bin\ + FileSystem + <_TargetId>Folder + net7.0-windows10.0.17763.0 + win-x64 + false + true + false + false + true + + \ No newline at end of file diff --git a/ControlApp/README.md b/ControlApp/README.md new file mode 100644 index 00000000..0086ad2f --- /dev/null +++ b/ControlApp/README.md @@ -0,0 +1,7 @@ +# ControlApp + +## Publish single-file release + +```PowerShell +dotnet publish /p:PublishProfile=Properties\PublishProfiles\release-win-x64.pubxml +``` diff --git a/ControlApp/Resources/AltRumbleMode.png b/ControlApp/Resources/AltRumbleMode.png new file mode 100644 index 00000000..a4f25ed9 Binary files /dev/null and b/ControlApp/Resources/AltRumbleMode.png differ diff --git a/ControlApp/Resources/NormalRumbleMode.png b/ControlApp/Resources/NormalRumbleMode.png new file mode 100644 index 00000000..dc610449 Binary files /dev/null and b/ControlApp/Resources/NormalRumbleMode.png differ diff --git a/ControlApp/Resources/Translations.cs b/ControlApp/Resources/Translations.cs new file mode 100644 index 00000000..7f80c3a6 --- /dev/null +++ b/ControlApp/Resources/Translations.cs @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +namespace Nefarius.DsHidMini.ControlApp.Resources +{ + public partial class Translations + { + } +} diff --git a/ControlApp/Resources/test.Designer.cs b/ControlApp/Resources/test.Designer.cs new file mode 100644 index 00000000..10be5eee --- /dev/null +++ b/ControlApp/Resources/test.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Nefarius.DsHidMini.ControlApp.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class test { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal test() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Nefarius.DsHidMini.ControlApp.Resources.test", typeof(test).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to . + /// + public static string HidModeEnumDS4W { + get { + return ResourceManager.GetString("HidModeEnumDS4W", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + public static string HidModeEnumGPJ { + get { + return ResourceManager.GetString("HidModeEnumGPJ", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + public static string HidModeEnumSDF { + get { + return ResourceManager.GetString("HidModeEnumSDF", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + public static string HidModeEnumSXS { + get { + return ResourceManager.GetString("HidModeEnumSXS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + public static string HidModeEnumUnknown { + get { + return ResourceManager.GetString("HidModeEnumUnknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + public static string HidModeEnumXInput { + get { + return ResourceManager.GetString("HidModeEnumXInput", resourceCulture); + } + } + } +} diff --git a/ControlApp/Resources/test.resx b/ControlApp/Resources/test.resx new file mode 100644 index 00000000..0afb03c8 --- /dev/null +++ b/ControlApp/Resources/test.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ControlApp/Services/AppSnackbarMessagesService.cs b/ControlApp/Services/AppSnackbarMessagesService.cs new file mode 100644 index 00000000..c748a331 --- /dev/null +++ b/ControlApp/Services/AppSnackbarMessagesService.cs @@ -0,0 +1,139 @@ +using Nefarius.Utilities.DeviceManagement.PnP; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Wpf.Ui; +using Wpf.Ui.Controls; + +namespace Nefarius.DsHidMini.ControlApp.Services +{ + public class AppSnackbarMessagesService + { + + private readonly ISnackbarService _snackbarService; + + public AppSnackbarMessagesService(ISnackbarService snackbarService) + { + _snackbarService = snackbarService; + } + + public void ShowDsHidMiniConfigurationUpdateSuccessMessage() + { + _snackbarService.Show( + "DsHidMini configuration updated", + "", + ControlAppearance.Success, + new SymbolIcon(SymbolRegular.CheckmarkCircle24), + TimeSpan.FromSeconds(2) + ); + } + + public void ShowDsHidMiniConfigurationUpdateFailedMessage() + { + _snackbarService.Show( + "Failed to updated DsHidMini configuration", + "", + ControlAppearance.Danger, + new SymbolIcon(SymbolRegular.DismissCircle24), + TimeSpan.FromSeconds(3) + ); + } + + public void ShowProfileDeletedMessage() + { + _snackbarService.Show( + "Profile deleted" + , "Devices using this profile reverted to global mode", + ControlAppearance.Caution, + new SymbolIcon(SymbolRegular.ErrorCircle24), + TimeSpan.FromSeconds(5) + ); + } + + public void ShowGlobalProfileUpdatedMessage() + { + _snackbarService.Show( + "Global profile updated" + , "" + , ControlAppearance.Info, + new SymbolIcon(SymbolRegular.Checkmark24), + TimeSpan.FromSeconds(2) + ); + } + + public void ShowProfileUpdateMessage() + { + _snackbarService.Show( + "Profile updated", + "", + ControlAppearance.Info, + new SymbolIcon(SymbolRegular.CheckmarkCircle24), + TimeSpan.FromSeconds(5) + ); + } + + public void ShowDefaultProfileEditingBlockedMessage() + { + _snackbarService.Show( + "ControlApp's default profile can't be modified", + "", + ControlAppearance.Info, + new SymbolIcon(SymbolRegular.Info24), + TimeSpan.FromSeconds(2) + ); + } + + + public void ShowProfileChangedCanceledMessage() + { + _snackbarService.Show( + "Canceled profile changes", + "Remember to save next time", + ControlAppearance.Caution, + new SymbolIcon(SymbolRegular.ErrorCircle24), + TimeSpan.FromSeconds(5) + ); + } + + public void ShowPowerCyclingDeviceMessage(bool isWireless, bool isAppElevated, bool reconnectionResult) + { + string temp = ""; + if(!isWireless && !isAppElevated) + { + _snackbarService.Show( + $"Auto USB restart denied", + "Restarting USB controller requires the ControlApp to be running as administrator", + ControlAppearance.Caution, + new SymbolIcon(SymbolRegular.ErrorCircle24), + TimeSpan.FromSeconds(8) + ); + return; + } + + if (reconnectionResult) + { + _snackbarService.Show( + $"Restarting (USB) / Disconnecting (bluetooth) device", + "", + ControlAppearance.Info, + new SymbolIcon(SymbolRegular.Info24), + TimeSpan.FromSeconds(4) + ); + } + else + { + _snackbarService.Show( + $"Failed to restart (USB) / Disconnect (bluetooth) device", + "Manually reconnecting the device might be required to update its HID mode.", + ControlAppearance.Caution, + new SymbolIcon(SymbolRegular.ErrorCircle24), + TimeSpan.FromSeconds(8) + ); + } + + + } + } +} diff --git a/ControlApp/Services/ApplicationHostService.cs b/ControlApp/Services/ApplicationHostService.cs new file mode 100644 index 00000000..8118321f --- /dev/null +++ b/ControlApp/Services/ApplicationHostService.cs @@ -0,0 +1,63 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using Microsoft.Extensions.Hosting; +using Nefarius.DsHidMini.ControlApp.Views.Pages; +using Nefarius.DsHidMini.ControlApp.Views.Windows; +using Wpf.Ui; + +namespace Nefarius.DsHidMini.ControlApp.Services; + +/// +/// Managed host of the application. +/// +public class ApplicationHostService : IHostedService +{ + private readonly IServiceProvider _serviceProvider; + private INavigationWindow _navigationWindow; + + public ApplicationHostService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + /// Triggered when the application host is ready to start the service. + /// + /// Indicates that the start process has been aborted. + public async Task StartAsync(CancellationToken cancellationToken) + { + await HandleActivationAsync(); + } + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// + /// Indicates that the shutdown process should no longer be graceful. + public async Task StopAsync(CancellationToken cancellationToken) + { + await Task.CompletedTask; + } + + /// + /// Creates main window during activation. + /// + private async Task HandleActivationAsync() + { + await Task.CompletedTask; + + if (!Application.Current.Windows.OfType().Any()) + { + _navigationWindow = ( + _serviceProvider.GetService(typeof(MainWindow)) as INavigationWindow + )!; + _navigationWindow!.ShowWindow(); + + _navigationWindow.Navigate(typeof(DevicesPage)); + } + + await Task.CompletedTask; + } +} diff --git a/ControlApp/Usings.cs b/ControlApp/Usings.cs new file mode 100644 index 00000000..5efad984 --- /dev/null +++ b/ControlApp/Usings.cs @@ -0,0 +1,4 @@ +global using CommunityToolkit.Mvvm.ComponentModel; +global using CommunityToolkit.Mvvm.Input; +global using System; +global using System.Windows; diff --git a/ControlApp/ViewModels/Pages/DevicesViewModel.cs b/ControlApp/ViewModels/Pages/DevicesViewModel.cs new file mode 100644 index 00000000..c559bbb0 --- /dev/null +++ b/ControlApp/ViewModels/Pages/DevicesViewModel.cs @@ -0,0 +1,283 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using Nefarius.DsHidMini.ControlApp.Models.Drivers; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.Utilities.DeviceManagement.PnP; +using System.Collections.ObjectModel; +using System.Diagnostics; + +using Nefarius.DsHidMini.ControlApp.Models; +using Nefarius.DsHidMini.ControlApp.Services; +using Wpf.Ui.Controls; +using Newtonsoft.Json.Linq; + +using Serilog; + +using Wpf.Ui; +using Wpf.Ui.Extensions; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.Pages +{ + public partial class DevicesViewModel : ObservableObject, INavigationAware + { + + + /// + /// List of detected devices. + /// + public ObservableCollection Devices { get; set; } = new(); + + private readonly DshmDevMan _dshmDevMan; + private readonly DshmConfigManager _dshmConfigManager; + private readonly AppSnackbarMessagesService _appSnackbarMessagesService; + private readonly IContentDialogService _contentDialogService; + + + /// + /// Currently selected device, if any. + /// + [ObservableProperty] private DeviceViewModel? _selectedDevice; + + /// + /// Is a device currently selected. + /// + public bool HasDeviceSelected => SelectedDevice != null; + + /// + /// Are there devices connected. + /// + [ObservableProperty] private bool _anyDeviceSelected = false; + + public DevicesViewModel(DshmDevMan dshmDevMan, DshmConfigManager dshmConfigManager, AppSnackbarMessagesService appSnackbarMessagesService, IContentDialogService contentDialogService) + { + _dshmDevMan = dshmDevMan; + _dshmConfigManager = dshmConfigManager; + _appSnackbarMessagesService = appSnackbarMessagesService; + _dshmDevMan.ConnectedDeviceListUpdated += OnConnectedDevicesListUpdated; + _dshmConfigManager.DshmConfigurationUpdated += OnDshmConfigUpdated; + _contentDialogService = contentDialogService; + RefreshDevicesList(); + } + + + partial void OnSelectedDeviceChanged(DeviceViewModel? value) + { + AnyDeviceSelected = (value != null); + } + + public void OnNavigatedTo() + { + Log.Logger.Debug("Navigating to Devices page. Refreshing dynamic properties of each connected Device ViewModel."); + foreach(DeviceViewModel device in Devices) + { + device.RefreshDeviceSettings(); + } + } + + public void OnNavigatedFrom() + { + SelectedDevice = null; + } + + public void OnConnectedDevicesListUpdated(object? obj, EventArgs? eventArgs) + { + RefreshDevicesList(); + } + + public void OnDshmConfigUpdated(object? obj, EventArgs? eventArgs) + { + ReconnectDevicesWithMismatchedHidMode(); + } + + public void ReconnectAllDevices() + { + bool oneOrMoreFails = false; + foreach (DeviceViewModel devVM in Devices) + { + if (devVM.IsHidModeMismatched) + { + if (!_dshmDevMan.TryReconnectDevice(devVM.Device)){ + oneOrMoreFails = true; + } + } + } + if(oneOrMoreFails) + { + ShowReconnectionAttemptFailedMessageBox(); + } + } + + private async void ShowReconnectionAttemptFailedMessageBox() + { + var uiMessageBox = new Wpf.Ui.Controls.MessageBox + { + Title = "Failed to restart/reconnect one or more devices", + Content = "To restart USB controllers the ControlApp needs to be running as administrator.\n\nIf it's still failing then reconnect the controllers manually to update their HID mode.", + }; + + _ = await uiMessageBox.ShowDialogAsync(); + } + + private void ReconnectDevicesWithMismatchedHidMode() + { + Log.Logger.Debug("Checking for devices in non-expected Hid Mode"); + bool mismatchedRemains = false; + foreach (DeviceViewModel devVM in Devices) + { + if (devVM.IsHidModeMismatched) + { + mismatchedRemains = true; + break; + } + } + + if (mismatchedRemains) + { + Log.Logger.Information("Detected one or more devices with non-expected Hid Mode."); + ShowHidMismatchedDevicesDialog(); + } + else + { + Log.Logger.Debug("No mismatches found"); + } + } + + private async void ShowHidMismatchedDevicesDialog() + { + Log.Logger.Debug("Showing Hid Mismatch dialog."); + var result = await _contentDialogService.ShowSimpleDialogAsync( + new SimpleContentDialogCreateOptions() + { + Title = "One or more devices pending reconnection", + Content = @"The HID mode setting of one or more connected device has changed. +Reconnect the pending devices to make this change effective.", + //PrimaryButtonText = "Ok", + //SecondaryButtonText = "Don't Save", + PrimaryButtonText = "Reconnect them now", + CloseButtonText = "OK", + } + ); + if (result == ContentDialogResult.Primary) + { + ReconnectAllDevices(); + } + + } + + private void RefreshDevicesList() + { + App.Current.Dispatcher.BeginInvoke(new Action(() => + { + Devices.Clear(); + foreach (PnPDevice device in _dshmDevMan.Devices) + { + Devices.Add(new DeviceViewModel(device, _dshmDevMan, _dshmConfigManager, _appSnackbarMessagesService, _contentDialogService)); + } + })); + } + + + /* + /// + /// Helper to check if run with elevated privileges. + /// + public bool IsElevated => SecurityUtil.IsElevated; + + /// + /// True if run as regular, non-administrative user. + /// + public bool IsMissingPermissions => !IsElevated; + + /// + /// Is it possible to edit the selected device. + /// + public bool IsEditable => IsElevated && HasDeviceSelected && !IsRestarting; + + /// + /// Is the selected device in the process of getting restarted. + /// + public bool IsRestarting { get; set; } = false; + + /// + /// Version to display in window title. + /// + public string Version => $"Version: {Assembly.GetEntryAssembly().GetName().Version}"; + + /// + /// True if GitHub version is newer than own version. + /// + public bool IsUpdateAvailable => Updater.IsUpdateAvailable; + + private static string ParametersKey => + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\WUDF\\Services\\dshidmini\\Parameters"; + + /// + /// Indicates if verbose logging is on or off. + /// + public bool VerboseOn + { + get + { + using (var key = RegistryHelpers.GetRegistryKey(ParametersKey)) + { + if (key == null) return false; + var value = key.GetValue("VerboseOn"); + return value != null && (int) value > 0; + } + } + set + { + using (var key = RegistryHelpers.GetRegistryKey(ParametersKey, true)) + { + key?.SetValue("VerboseOn", value ? 1 : 0, RegistryValueKind.DWord); + } + } + } + + public bool IsFilterAvailable => IsElevated && BthPS3FilterDriver.IsFilterAvailable; + + public bool IsFilterUnavailable => IsElevated && !BthPS3FilterDriver.IsFilterAvailable; + + public bool IsFilterEnabled + { + get => IsElevated && BthPS3FilterDriver.IsFilterEnabled; + set => BthPS3FilterDriver.IsFilterEnabled = value; + } + + public bool IsRawPDODisabled => !BthPS3ProfileDriver.RawPDO; + + public bool AreBthPS3SettingsCorrect => + IsElevated && !BthPS3ProfileDriver.RawPDO && BthPS3FilterDriver.IsFilterEnabled; + + public bool AreBthPS3SettingsIncorrect => + IsElevated && (BthPS3ProfileDriver.RawPDO || !BthPS3FilterDriver.IsFilterEnabled); + + public bool IsPressureMutingSupported => IsEditable && SelectedDevice.IsPressureMutingSupported; + + public event PropertyChangedEventHandler PropertyChanged; + + public void RefreshProperties() + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsFilterEnabled")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsRawPDODisabled")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AreBthPS3SettingsIncorrect")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AreBthPS3SettingsCorrect")); + } + */ + } +} + +//[ObservableProperty] +//private int _counter = 0; + + + +//[RelayCommand] +//private void OnCounterIncrement() +//{ +// Counter++; +//} + diff --git a/ControlApp/ViewModels/Pages/ProfilesViewModel.cs b/ControlApp/ViewModels/Pages/ProfilesViewModel.cs new file mode 100644 index 00000000..8c2ff18a --- /dev/null +++ b/ControlApp/ViewModels/Pages/ProfilesViewModel.cs @@ -0,0 +1,129 @@ +using System.Diagnostics.Metrics; +using Nefarius.DsHidMini.ControlApp.Models; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Services; + +using Serilog; + +using Wpf.Ui; +using Wpf.Ui.Controls; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.Pages +{ + + public partial class ProfilesViewModel : ObservableObject, INavigationAware + { + // ----------------------------------------------------------- FIELDS + + private readonly DshmDevMan _dshmDevMan; + private readonly DshmConfigManager _dshmConfigManager; + + [ObservableProperty] public List _profilesViewModels; + [ObservableProperty] private ProfileViewModel? _selectedProfileVM = null; + + + + // ----------------------------------------------------------- PROPERTIES + + public List ProfilesDatas => _dshmConfigManager.GetListOfProfilesWithDefault(); + + private readonly AppSnackbarMessagesService _appSnackbarMessagesService; + + [ObservableProperty] private bool _anyProfileSelected; + + // ----------------------------------------------------------- CONSTRUCTOR + + public ProfilesViewModel(AppSnackbarMessagesService appSnackbarMessagesService, DshmDevMan dshmDevMan, DshmConfigManager dshmConfigManager) + { + _dshmDevMan = dshmDevMan; + _dshmConfigManager = dshmConfigManager; + _appSnackbarMessagesService = appSnackbarMessagesService; + UpdateProfileList(); + } + + public void UpdateProfileList() + { + Log.Logger.Debug("Rebuilding profiles' ViewModels."); + List newList = new(); + foreach(ProfileData prof in ProfilesDatas) + { + newList.Add(new(prof, _appSnackbarMessagesService, _dshmConfigManager)); + } + ProfilesViewModels = newList; + UpdateGlobalProfileCheck(); + } + + public void UpdateGlobalProfileCheck() + { + Log.Logger.Debug("Updating Profiles' ViewModels 'Global' check"); + foreach (ProfileViewModel profVM in ProfilesViewModels) + { + profVM.IsGlobal = (profVM.ProfileData == _dshmConfigManager.GlobalProfile); + } + } + + + // ---------------------------------------- Methods + + public void OnNavigatedFrom() + { + if(SelectedProfileVM != null && SelectedProfileVM.IsEditEnabled) + { + Log.Logger.Information("Navigated away from Profiles page mid-edition. Canceling changes."); + SelectedProfileVM.CancelChanges(); + _appSnackbarMessagesService.ShowProfileChangedCanceledMessage(); + } + + SelectedProfileVM = null; + } + + public void OnNavigatedTo() + { + } + + partial void OnSelectedProfileVMChanged(ProfileViewModel? value) + { + AnyProfileSelected = (SelectedProfileVM != null); + } + + [RelayCommand] + private void SetprofileAsGlobal(ProfileViewModel? obj) + { + if (obj != null) + { + Log.Logger.Information($"Setting profile '{obj.ProfileData.ProfileName}' ({obj.ProfileData.ProfileGuid}) as Global."); + _dshmConfigManager.GlobalProfile = obj.ProfileData; + _appSnackbarMessagesService.ShowGlobalProfileUpdatedMessage(); + _dshmConfigManager.SaveChangesAndUpdateDsHidMiniConfigFile(); + UpdateGlobalProfileCheck(); + } + } + + [RelayCommand] + private void CreateProfile() + { + Log.Logger.Information("Creating new profile generic name."); + _dshmConfigManager.CreateProfile("New profile"); + _dshmConfigManager.SaveChanges(); + UpdateProfileList(); + } + + [RelayCommand] + private void DeleteProfile(ProfileViewModel? obj) + { + if (obj == null) return; + Log.Logger.Information($"Deleting profile '{obj.ProfileData.ProfileName}' ({obj.ProfileData.ProfileGuid})"); + if (obj.ProfileData == ProfileData.DefaultProfile) + { + Log.Logger.Information("Profile to be deleted is ControlApp's Default Profile. Delete action canceled."); + _appSnackbarMessagesService.ShowDefaultProfileEditingBlockedMessage(); + return; + } + _dshmConfigManager.DeleteProfile(obj.ProfileData); + _dshmConfigManager.SaveChangesAndUpdateDsHidMiniConfigFile(); + _appSnackbarMessagesService.ShowProfileDeletedMessage(); + UpdateProfileList(); + } + + } +} \ No newline at end of file diff --git a/ControlApp/ViewModels/Pages/SettingsViewModel.cs b/ControlApp/ViewModels/Pages/SettingsViewModel.cs new file mode 100644 index 00000000..4ad6b963 --- /dev/null +++ b/ControlApp/ViewModels/Pages/SettingsViewModel.cs @@ -0,0 +1,67 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using Wpf.Ui.Controls; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.Pages +{ + public partial class SettingsViewModel : ObservableObject, INavigationAware + { + private bool _isInitialized = false; + + [ObservableProperty] + private string _appVersion = String.Empty; + + [ObservableProperty] + private Wpf.Ui.Appearance.ApplicationTheme _currentTheme = Wpf.Ui.Appearance.ApplicationTheme.Unknown; + + public void OnNavigatedTo() + { + if (!_isInitialized) + InitializeViewModel(); + } + + public void OnNavigatedFrom() { } + + private void InitializeViewModel() + { + CurrentTheme = Wpf.Ui.Appearance.ApplicationThemeManager.GetAppTheme(); + AppVersion = $"Dshm_ControlApp_WpfUi - {GetAssemblyVersion()}"; + + _isInitialized = true; + } + + private string GetAssemblyVersion() + { + return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() + ?? String.Empty; + } + + [RelayCommand] + private void OnChangeTheme(string parameter) + { + switch (parameter) + { + case "theme_light": + if (CurrentTheme == Wpf.Ui.Appearance.ApplicationTheme.Light) + break; + + Wpf.Ui.Appearance.ApplicationThemeManager.Apply(Wpf.Ui.Appearance.ApplicationTheme.Light); + CurrentTheme = Wpf.Ui.Appearance.ApplicationTheme.Light; + + break; + + default: + if (CurrentTheme == Wpf.Ui.Appearance.ApplicationTheme.Dark) + break; + + Wpf.Ui.Appearance.ApplicationThemeManager.Apply(Wpf.Ui.Appearance.ApplicationTheme.Dark); + CurrentTheme = Wpf.Ui.Appearance.ApplicationTheme.Dark; + + break; + } + } + } +} diff --git a/ControlApp/ViewModels/UserControls/DeviceSettings/AltRumbleModeSettingsViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettings/AltRumbleModeSettingsViewModel.cs new file mode 100644 index 00000000..137c7420 --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettings/AltRumbleModeSettingsViewModel.cs @@ -0,0 +1,89 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings +{ + public class AltRumbleModeSettingsViewModel : DeviceSettingsViewModel + { + // -------------------------------------------- RIGHT MOTOR CONVERSION GROUP + + public AltRumbleModeSettings _tempBackingData = new(); + protected override DeviceSubSettings _mySubSetting => _tempBackingData; + + public override SettingsModeGroups Group { get; } = SettingsModeGroups.RumbleRightConversion; + + public int RightRumbleConversionUpperRange + { + get => _tempBackingData.RightRumbleConversionUpperRange; + set + { + int tempInt = (value < _tempBackingData.RightRumbleConversionLowerRange) ? _tempBackingData.RightRumbleConversionLowerRange + 1 : value; + _tempBackingData.RightRumbleConversionUpperRange = tempInt; + this.OnPropertyChanged(nameof(RightRumbleConversionUpperRange)); + } + } + public int RightRumbleConversionLowerRange + { + get => _tempBackingData.RightRumbleConversionLowerRange; + set + { + int tempInt = (value > _tempBackingData.RightRumbleConversionUpperRange) ? (byte)(_tempBackingData.RightRumbleConversionUpperRange - 1) : value; + _tempBackingData.RightRumbleConversionLowerRange = tempInt; + this.OnPropertyChanged(nameof(RightRumbleConversionLowerRange)); + } + } + public bool IsForcedRightMotorLightThresholdEnabled + { + get => _tempBackingData.IsForcedRightMotorLightThresholdEnabled; + set + { + _tempBackingData.IsForcedRightMotorLightThresholdEnabled = value; + this.OnPropertyChanged(nameof(IsForcedRightMotorLightThresholdEnabled)); + } + } + public bool IsForcedRightMotorHeavyThreasholdEnabled + { + get => _tempBackingData.IsForcedRightMotorHeavyThreasholdEnabled; + set + { + _tempBackingData.IsForcedRightMotorHeavyThreasholdEnabled = value; + this.OnPropertyChanged(nameof(IsForcedRightMotorHeavyThreasholdEnabled)); + } + } + public int ForcedRightMotorLightThreshold + { + get => _tempBackingData.ForcedRightMotorLightThreshold; + set + { + _tempBackingData.ForcedRightMotorLightThreshold = value; + this.OnPropertyChanged(nameof(ForcedRightMotorLightThreshold)); + } + } + public int ForcedRightMotorHeavyThreshold + { + get => _tempBackingData.ForcedRightMotorHeavyThreshold; + set + { + _tempBackingData.ForcedRightMotorHeavyThreshold = value; + this.OnPropertyChanged(nameof(ForcedRightMotorHeavyThreshold)); + } + } + + public AltRumbleModeSettingsViewModel() : base() + { + } + + //public override void SaveSettingsToBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_VariablaRightRumbleEmulAdjusts.CopySettings(dataContainerSource.AltRumbleAdjusts, _tempBackingData); + //} + + //public override void LoadSettingsFromBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_VariablaRightRumbleEmulAdjusts.CopySettings(_tempBackingData, dataContainerSource.AltRumbleAdjusts); + // NotifyAllPropertiesHaveChanged(); + //} + } + + +} diff --git a/ControlApp/ViewModels/UserControls/DeviceSettings/DeviceSettingsViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettings/DeviceSettingsViewModel.cs new file mode 100644 index 00000000..1a281a55 --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettings/DeviceSettingsViewModel.cs @@ -0,0 +1,148 @@ +using System.Windows.Ink; + +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings; + +public abstract partial class DeviceSettingsViewModel : ObservableObject +{ + /// Replace with LexLoc + private static Dictionary DictGroupHeader = new() + { + { SettingsModeGroups.LEDsControl, "LEDs control" }, + { SettingsModeGroups.WirelessSettings, "Wireless" }, + { SettingsModeGroups.SticksDeadzone, "Sticks DeadZone" }, + { SettingsModeGroups.RumbleGeneral, "Rumble" }, + { SettingsModeGroups.OutputReportControl, "Output report control" }, + { SettingsModeGroups.RumbleLeftStrRescale, "Left motor rescale" }, + { SettingsModeGroups.RumbleRightConversion, "Alternative rumble mode adjuster" }, + { SettingsModeGroups.Unique_All, "HID Mode" }, + { SettingsModeGroups.Unique_Global, "Default settings" }, + { SettingsModeGroups.Unique_General, "General settings" }, + { SettingsModeGroups.Unique_SDF, "SDF mode specific settings" }, + { SettingsModeGroups.Unique_GPJ, "GPJ mode specific settings" }, + { SettingsModeGroups.Unique_SXS, "SXS mode specific settings" }, + { SettingsModeGroups.Unique_DS4W, "DS4W mode specific settings" }, + { SettingsModeGroups.Unique_XInput, "GPJ mode specific settings" }, + }; + + [ObservableProperty] private List _controlAppProfiles = new List() { "profile 1", "profile 2", "profile 3" }; + + public abstract SettingsModeGroups Group { get; } + + protected abstract DeviceSubSettings _mySubSetting { get; } + + [ObservableProperty] private bool _isGroupLocked = false; + + public string? Header { get; } + + [RelayCommand] + public void ResetGroupToOriginalDefaults() + { + _mySubSetting.ResetToDefault(); + NotifyAllPropertiesHaveChanged(); + } + + public virtual void NotifyAllPropertiesHaveChanged() + { + this.OnPropertyChanged(string.Empty); + } + + public void LoadSettingsFromBackingDataContainer(Models.DshmConfigManager.DeviceSettings dataContainerSource) + { + if (_mySubSetting is HidModeSettings) HidModeSettings.CopySettings((HidModeSettings)_mySubSetting, dataContainerSource.HidMode); + if (_mySubSetting is LedsSettings) LedsSettings.CopySettings((LedsSettings)_mySubSetting, dataContainerSource.LEDs); + if (_mySubSetting is WirelessSettings) WirelessSettings.CopySettings((WirelessSettings)_mySubSetting, dataContainerSource.Wireless); + if (_mySubSetting is SticksSettings) SticksSettings.CopySettings((SticksSettings)_mySubSetting, dataContainerSource.Sticks); + if (_mySubSetting is GeneralRumbleSettings) GeneralRumbleSettings.CopySettings((GeneralRumbleSettings)_mySubSetting, dataContainerSource.GeneralRumble); + if (_mySubSetting is OutputReportSettings) OutputReportSettings.CopySettings((OutputReportSettings)_mySubSetting, dataContainerSource.OutputReport); + if (_mySubSetting is LeftMotorRescalingSettings) LeftMotorRescalingSettings.CopySettings((LeftMotorRescalingSettings)_mySubSetting, dataContainerSource.LeftMotorRescaling); + if (_mySubSetting is AltRumbleModeSettings) AltRumbleModeSettings.CopySettings((AltRumbleModeSettings)_mySubSetting, dataContainerSource.AltRumbleAdjusts); + NotifyAllPropertiesHaveChanged(); + } + + public void SaveSettingsToBackingDataContainer(Models.DshmConfigManager.DeviceSettings dataContainerSource) + { + if (_mySubSetting is HidModeSettings) HidModeSettings.CopySettings(dataContainerSource.HidMode, (HidModeSettings)_mySubSetting); + if (_mySubSetting is LedsSettings) LedsSettings.CopySettings(dataContainerSource.LEDs, (LedsSettings)_mySubSetting); + if (_mySubSetting is WirelessSettings) WirelessSettings.CopySettings(dataContainerSource.Wireless, (WirelessSettings)_mySubSetting); + if (_mySubSetting is SticksSettings) SticksSettings.CopySettings(dataContainerSource.Sticks,(SticksSettings)_mySubSetting); + if (_mySubSetting is GeneralRumbleSettings) GeneralRumbleSettings.CopySettings(dataContainerSource.GeneralRumble, (GeneralRumbleSettings)_mySubSetting); + if (_mySubSetting is OutputReportSettings) OutputReportSettings.CopySettings(dataContainerSource.OutputReport, (OutputReportSettings)_mySubSetting); + if (_mySubSetting is LeftMotorRescalingSettings) LeftMotorRescalingSettings.CopySettings(dataContainerSource.LeftMotorRescaling, (LeftMotorRescalingSettings)_mySubSetting); + if (_mySubSetting is AltRumbleModeSettings) AltRumbleModeSettings.CopySettings(dataContainerSource.AltRumbleAdjusts, (AltRumbleModeSettings)_mySubSetting); + } + + public DeviceSettingsViewModel() + { + if (DictGroupHeader.TryGetValue(Group, out string groupHeader)) Header = groupHeader; + //LoadSettingsFromBackingDataContainer(backingDataContainer); + } +} + +public partial class ButtonComboViewModel : ObservableObject +{ + private ButtonsCombo _buttonCombo; + + public bool IsEnabled + { + get => _buttonCombo.IsEnabled; + set + { + _buttonCombo.IsEnabled = value; + OnPropertyChanged(nameof(IsEnabled)); + } + } + + public int HoldTime + { + get => _buttonCombo.HoldTime / 1000; + set + { + _buttonCombo.HoldTime = value * 1000; + OnPropertyChanged(nameof(IsEnabled)); + } + } + + public int MinHoldTime { get; } + + public Button Button1 + { + get => _buttonCombo.ButtonCombo[0]; + set + { + _buttonCombo.ButtonCombo[0] = value; + OnPropertyChanged(nameof(Button1)); + } + } + public Button Button2 + { + get => _buttonCombo.ButtonCombo[1]; + set + { + _buttonCombo.ButtonCombo[1] = value; + OnPropertyChanged(nameof(Button2)); + } + } + public Button Button3 + { + get => _buttonCombo.ButtonCombo[2]; + set + { + _buttonCombo.ButtonCombo[2] = value; + OnPropertyChanged(nameof(Button3)); + } + } + + public ButtonComboViewModel(ButtonsCombo buttonsCombo, int minHoldTime) + { + _buttonCombo = buttonsCombo; + MinHoldTime = minHoldTime; + } + + public void NotifyAllPropertiesChanged() + { + this.OnPropertyChanged(nameof(string.Empty)); + } +} \ No newline at end of file diff --git a/ControlApp/ViewModels/UserControls/DeviceSettings/GeneralRumbleSettingsViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettings/GeneralRumbleSettingsViewModel.cs new file mode 100644 index 00000000..1bed2af1 --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettings/GeneralRumbleSettingsViewModel.cs @@ -0,0 +1,69 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings +{ + public class GeneralRumbleSettingsViewModel : DeviceSettingsViewModel + { + private GeneralRumbleSettings _tempBackingData = new(); + protected override DeviceSubSettings _mySubSetting => _tempBackingData; + + public override SettingsModeGroups Group { get; } = SettingsModeGroups.RumbleGeneral; + + public bool IsLeftMotorDisabled + { + get => _tempBackingData.IsLeftMotorDisabled; + + set + { + _tempBackingData.IsLeftMotorDisabled = value; + this.OnPropertyChanged(nameof(IsLeftMotorDisabled)); + } + } + public bool IsRightMotorDisabled + { + get => _tempBackingData.IsRightMotorDisabled; + + set + { + _tempBackingData.IsRightMotorDisabled = value; + this.OnPropertyChanged(nameof(IsRightMotorDisabled)); + } + } + + public bool IsAltModeEnabled + { + get => _tempBackingData.IsAltRumbleModeEnabled; + set + { + _tempBackingData.IsAltRumbleModeEnabled = value; + this.OnPropertyChanged(nameof(IsAltModeEnabled)); + } + } + + public bool AlwaysStartInNormalRumbleMode + { + get => _tempBackingData.AlwaysStartInNormalMode; + set + { + _tempBackingData.AlwaysStartInNormalMode = value; + this.OnPropertyChanged(nameof(AlwaysStartInNormalRumbleMode)); + } + } + + public ButtonComboViewModel AltModeToggleButtonCombo { get; set; } + + public GeneralRumbleSettingsViewModel() : base() + { + AltModeToggleButtonCombo = new(_tempBackingData.AltModeToggleButtonCombo, 1); + } + + public override void NotifyAllPropertiesHaveChanged() + { + base.NotifyAllPropertiesHaveChanged(); + AltModeToggleButtonCombo.NotifyAllPropertiesChanged(); + } + } + + +} diff --git a/ControlApp/ViewModels/UserControls/DeviceSettings/HidModeSettingsViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettings/HidModeSettingsViewModel.cs new file mode 100644 index 00000000..8d9546f6 --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettings/HidModeSettingsViewModel.cs @@ -0,0 +1,127 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings +{ + + public class HidModeSettingsViewModel : DeviceSettingsViewModel + { + public readonly List hidDeviceModesList = new List + { + SettingsContext.SDF, + SettingsContext.GPJ, + SettingsContext.SXS, + SettingsContext.DS4W, + SettingsContext.XInput, + }; + public static readonly List listOfPressureModes = new() + { + PressureMode.Digital, + PressureMode.Analogue, + PressureMode.Default, + }; + + public static readonly List listOfDPadModes = new() + { + DPadMode.HAT, + DPadMode.Buttons, + }; + public List HIDDeviceModesList => hidDeviceModesList; + public static List ListOfPressureModes { get => listOfPressureModes; } + public static List ListOfDPadModes { get => listOfDPadModes; } + + private HidModeSettings _tempBackingData = new(); + protected override DeviceSubSettings _mySubSetting => _tempBackingData; + + public override SettingsModeGroups Group { get; } = SettingsModeGroups.Unique_All; + + public SettingsContext Context + { + get => _tempBackingData.SettingsContext; + set + { + _tempBackingData.SettingsContext = value; + this.OnPropertyChanged(nameof(Context)); + } + + } + public PressureMode PressureExposureMode + { + get => _tempBackingData.PressureExposureMode; + set + { + _tempBackingData.PressureExposureMode = value; + this.OnPropertyChanged(nameof(PressureExposureMode)); + } + } + + public DPadMode DPadExposureMode + { + get => _tempBackingData.DPadExposureMode; + set + { + _tempBackingData.DPadExposureMode = value; + this.OnPropertyChanged(nameof(DPadExposureMode)); + } + } + + // SXS + public bool PreventRemappingConflictsInSXSMode + { + get => _tempBackingData.PreventRemappingConflictsInSXSMode; + set + { + _tempBackingData.PreventRemappingConflictsInSXSMode = value; + this.OnPropertyChanged(nameof(PreventRemappingConflictsInSXSMode)); + } + } + + public bool AllowAppsToControlLEDsInSXSMode + { + get => _tempBackingData.AllowAppsToOverrideLEDsInSXSMode; + set + { + _tempBackingData.AllowAppsToOverrideLEDsInSXSMode = value; + this.OnPropertyChanged(nameof(AllowAppsToControlLEDsInSXSMode)); + } + } + + // XInput + public bool IsLEDsAsXInputSlotEnabled + { + get => _tempBackingData.IsLEDsAsXInputSlotEnabled; + set + { + _tempBackingData.IsLEDsAsXInputSlotEnabled = value; + this.OnPropertyChanged(nameof(IsLEDsAsXInputSlotEnabled)); + } + } + + // DS4Windows + public bool PreventRemappingConflictsInDS4WMode + { + get => _tempBackingData.PreventRemappingConflictsInDS4WMode; + set + { + _tempBackingData.PreventRemappingConflictsInDS4WMode = value; + this.OnPropertyChanged(nameof(PreventRemappingConflictsInDS4WMode)); + } + } + + public HidModeSettingsViewModel() : base() + { + } + + + //public override void LoadSettingsFromBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_ModesUnique.CopySettings(_tempBackingData, dataContainerSource.SettingsContext); + // NotifyAllPropertiesHaveChanged(); + //} + + //public override void SaveSettingsToBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_ModesUnique.CopySettings(dataContainerSource.SettingsContext, _tempBackingData); + //} + } +} diff --git a/ControlApp/ViewModels/UserControls/DeviceSettings/LedsSettingsViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettings/LedsSettingsViewModel.cs new file mode 100644 index 00000000..90ac4e41 --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettings/LedsSettingsViewModel.cs @@ -0,0 +1,151 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; +using static Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.LedsSettings.All4LEDsCustoms; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings +{ + public partial class LedsSettingsViewModel : DeviceSettingsViewModel + { + private LedsSettings _tempBackingData = new(); + + public override SettingsModeGroups Group { get; } = SettingsModeGroups.LEDsControl; + public int LEDMode + { + get => (int)_tempBackingData.LeDMode; + set + { + _tempBackingData.LeDMode = (LEDsMode)value; + this.OnPropertyChanged(nameof(LEDMode)); + } + } + + protected override DeviceSubSettings _mySubSetting => _tempBackingData; + + public bool AllowLedsOverride + { + get => _tempBackingData.AllowExternalLedsControl; + set + { + _tempBackingData.AllowExternalLedsControl = value; + this.OnPropertyChanged(nameof(AllowLedsOverride)); + } + } + + [ObservableProperty] int _currentLEDCustomsIndex = 0; + + [ObservableProperty] LED_VM? _selectedLED_VM = null; + + [ObservableProperty] private LED_VM[]? _leds_VM = new LED_VM[] { new (1), new (2), new (3), new (4), }; + + partial void OnCurrentLEDCustomsIndexChanged(int value) + { + SelectedLED_VM = Leds_VM[value]; + } + + public LedsSettingsViewModel() + { + Leds_VM[0].singleLEDCustoms = _tempBackingData.LEDsCustoms.LED_x_Customs[0]; + Leds_VM[1].singleLEDCustoms = _tempBackingData.LEDsCustoms.LED_x_Customs[1]; + Leds_VM[2].singleLEDCustoms = _tempBackingData.LEDsCustoms.LED_x_Customs[2]; + Leds_VM[3].singleLEDCustoms = _tempBackingData.LEDsCustoms.LED_x_Customs[3]; + SelectedLED_VM = Leds_VM[0]; + } + + public override void NotifyAllPropertiesHaveChanged() + { + base.NotifyAllPropertiesHaveChanged(); + foreach (LED_VM ledVM in Leds_VM) + { + ledVM.RaisePropertyChangedForAll(); + } + } + + + //public override void SaveSettingsToBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_LEDs.CopySettings(dataContainerSource.LEDs, _tempBackingData); + //} + + //public override void LoadSettingsFromBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_LEDs.CopySettings(_tempBackingData, dataContainerSource.LEDs); + // NotifyAllPropertiesHaveChanged(); + //} + + + public partial class LED_VM : ObservableObject + { + public singleLEDCustoms singleLEDCustoms; + + [ObservableProperty] int _ledOrder = 0; + public bool IsEnabled + { + get => singleLEDCustoms.IsLedEnabled; + set + { + singleLEDCustoms.IsLedEnabled = value; + this.OnPropertyChanged(nameof(IsEnabled)); + } + } + public bool UseEffects + { + get => singleLEDCustoms.UseLEDEffects; + set + { + singleLEDCustoms.UseLEDEffects = value; + this.OnPropertyChanged(nameof(UseEffects)); + } + } + public byte Duration + { + get => singleLEDCustoms.Duration; + set + { + singleLEDCustoms.Duration = value; + this.OnPropertyChanged(nameof(Duration)); + } + } + public int IntervalDuration + { + get => singleLEDCustoms.CycleDuration; + set + { + singleLEDCustoms.CycleDuration = value; + + this.OnPropertyChanged(nameof(IntervalDuration)); + } + } + public int IntervalPortionON + { + get => singleLEDCustoms.OnPeriodCycles; + set + { + singleLEDCustoms.OnPeriodCycles = (byte)value; + this.OnPropertyChanged(nameof(IntervalPortionON)); + } + } + public int IntervalPortionOFF + { + get => singleLEDCustoms.OffPeriodCycles; + set + { + singleLEDCustoms.OffPeriodCycles = (byte)value; + this.OnPropertyChanged(nameof(IntervalPortionOFF)); + } + } + + public LED_VM(int ledOrder) + { + _ledOrder = ledOrder; + } + + public void RaisePropertyChangedForAll() + { + this.OnPropertyChanged(string.Empty); + } + } + + } + + +} diff --git a/ControlApp/ViewModels/UserControls/DeviceSettings/LeftMotorRescalingSettingsViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettings/LeftMotorRescalingSettingsViewModel.cs new file mode 100644 index 00000000..9ae48c88 --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettings/LeftMotorRescalingSettingsViewModel.cs @@ -0,0 +1,58 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings +{ + public class LeftMotorRescalingSettingsViewModel : DeviceSettingsViewModel + { + private LeftMotorRescalingSettings _tempBackingData = new(); + protected override DeviceSubSettings _mySubSetting => _tempBackingData; + + public override SettingsModeGroups Group { get; } = SettingsModeGroups.RumbleLeftStrRescale; + + public bool IsLeftMotorStrRescalingEnabled + { + get => _tempBackingData.IsLeftMotorStrRescalingEnabled; + set + { + _tempBackingData.IsLeftMotorStrRescalingEnabled = value; + this.OnPropertyChanged(nameof(IsLeftMotorStrRescalingEnabled)); + } + } + public int LeftMotorStrRescalingUpperRange + { + get => _tempBackingData.LeftMotorStrRescalingUpperRange; + set + { + _tempBackingData.LeftMotorStrRescalingUpperRange = (value < _tempBackingData.LeftMotorStrRescalingLowerRange) ? _tempBackingData.LeftMotorStrRescalingLowerRange + 1 : value; + this.OnPropertyChanged(nameof(LeftMotorStrRescalingUpperRange)); + } + } + public int LeftMotorStrRescalingLowerRange + { + get => _tempBackingData.LeftMotorStrRescalingLowerRange; + set + { + _tempBackingData.LeftMotorStrRescalingLowerRange = (value > _tempBackingData.LeftMotorStrRescalingUpperRange) ? _tempBackingData.LeftMotorStrRescalingUpperRange - 1 : value; + this.OnPropertyChanged(nameof(LeftMotorStrRescalingLowerRange)); + } + } + + public LeftMotorRescalingSettingsViewModel() : base() + { + } + + //public override void SaveSettingsToBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_LeftRumbleRescale.CopySettings(dataContainerSource.LeftMotorRescaling, _tempBackingData); + //} + + //public override void LoadSettingsFromBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_LeftRumbleRescale.CopySettings(_tempBackingData, dataContainerSource.LeftMotorRescaling); + // NotifyAllPropertiesHaveChanged(); + //} + } + + +} diff --git a/ControlApp/ViewModels/UserControls/DeviceSettings/OutputReportSettingsViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettings/OutputReportSettingsViewModel.cs new file mode 100644 index 00000000..e44dfb79 --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettings/OutputReportSettingsViewModel.cs @@ -0,0 +1,58 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings +{ + public class OutputReportSettingsViewModel : DeviceSettingsViewModel + { + private OutputReportSettings _tempBackingData = new(); + protected override DeviceSubSettings _mySubSetting => _tempBackingData; + public override SettingsModeGroups Group { get; } = SettingsModeGroups.OutputReportControl; + public bool IsOutputReportRateControlEnabled + { + get => _tempBackingData.IsOutputReportRateControlEnabled; + set + { + _tempBackingData.IsOutputReportRateControlEnabled = value; + this.OnPropertyChanged(nameof(IsOutputReportRateControlEnabled)); + } + } + + public int MaxOutputRate + { + get => _tempBackingData.MaxOutputRate; + set + { + _tempBackingData.MaxOutputRate = value; + this.OnPropertyChanged(nameof(MaxOutputRate)); + } + } + + public bool IsOutputReportDeduplicatorEnabled + { + get => _tempBackingData.IsOutputReportDeduplicatorEnabled; + set + { + _tempBackingData.IsOutputReportDeduplicatorEnabled = value; + this.OnPropertyChanged(nameof(IsOutputReportDeduplicatorEnabled)); + } + } + + public OutputReportSettingsViewModel() : base() + { + } + + //public override void SaveSettingsToBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_OutRepControl.CopySettings(dataContainerSource.OutputReport, _tempBackingData); + //} + + //public override void LoadSettingsFromBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_OutRepControl.CopySettings(_tempBackingData, dataContainerSource.OutputReport); + // NotifyAllPropertiesHaveChanged(); + //} + } + + +} diff --git a/ControlApp/ViewModels/UserControls/DeviceSettings/SticksSettingsViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettings/SticksSettingsViewModel.cs new file mode 100644 index 00000000..f8bf194d --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettings/SticksSettingsViewModel.cs @@ -0,0 +1,110 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings +{ + public class SticksSettingsViewModel : DeviceSettingsViewModel + { + // -------------------------------------------- STICKS DEADZONE GROUP + private SticksSettings _tempBackingData = new(); + protected override DeviceSubSettings _mySubSetting => _tempBackingData; + + public override SettingsModeGroups Group { get; } = SettingsModeGroups.SticksDeadzone; + + public bool ApplyLeftStickDeadZone + { + get => _tempBackingData.LeftStickData.IsDeadZoneEnabled; + set + { + _tempBackingData.LeftStickData.IsDeadZoneEnabled = value; + this.OnPropertyChanged(nameof(ApplyLeftStickDeadZone)); + } + } + + public bool ApplyRightStickDeadZone + { + get => _tempBackingData.RightStickData.IsDeadZoneEnabled; + set + { + _tempBackingData.RightStickData.IsDeadZoneEnabled = value; + this.OnPropertyChanged(nameof(ApplyRightStickDeadZone)); + } + } + + public int LeftStickDeadZone + { + get => _tempBackingData.LeftStickData.DeadZone; + set + { + _tempBackingData.LeftStickData.DeadZone = value; + this.OnPropertyChanged(nameof(LeftStickDeadZone)); + } + } + + public int RightStickDeadZone + { + get => _tempBackingData.RightStickData.DeadZone; + set + { + _tempBackingData.RightStickData.DeadZone = value; + this.OnPropertyChanged(nameof(RightStickDeadZone)); + } + } + + public bool InvertLSX + { + get => _tempBackingData.LeftStickData.InvertXAxis; + set + { + _tempBackingData.LeftStickData.InvertXAxis = value; + this.OnPropertyChanged(nameof(InvertLSX)); + } + } + + public bool InvertLSY + { + get => _tempBackingData.LeftStickData.InvertYAxis; + set + { + _tempBackingData.LeftStickData.InvertYAxis = value; + this.OnPropertyChanged(nameof(InvertLSY)); + } + } + + public bool InvertRSX + { + get => _tempBackingData.RightStickData.InvertXAxis; + set + { + _tempBackingData.RightStickData.InvertXAxis = value; + this.OnPropertyChanged(nameof(InvertRSX)); + } + } + + public bool InvertRSY + { + get => _tempBackingData.RightStickData.InvertYAxis; + set + { + _tempBackingData.RightStickData.InvertYAxis = value; + this.OnPropertyChanged(nameof(InvertRSY)); + } + } + + + public SticksSettingsViewModel() : base() + { + } + + //public override void SaveSettingsToBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_Sticks.CopySettings(dataContainerSource.Sticks, _tempBackingData); + //} + + //public override void LoadSettingsFromBackingDataContainer(BackingDataContainer dataContainerSource) + //{ + // BackingData_Sticks.CopySettings(_tempBackingData, dataContainerSource.Sticks); + // NotifyAllPropertiesHaveChanged(); + //} + } +} diff --git a/ControlApp/ViewModels/UserControls/DeviceSettings/WirelessSettingsViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettings/WirelessSettingsViewModel.cs new file mode 100644 index 00000000..9ab6e582 --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettings/WirelessSettingsViewModel.cs @@ -0,0 +1,48 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings +{ + public class WirelessSettingsViewModel : DeviceSettingsViewModel + { + private WirelessSettings _tempBackingData = new(); + protected override DeviceSubSettings _mySubSetting => _tempBackingData; + + public override SettingsModeGroups Group { get; } = SettingsModeGroups.WirelessSettings; + + public bool IsWirelessIdleDisconnectEnabled + { + get => _tempBackingData.IsWirelessIdleDisconnectEnabled; + set + { + _tempBackingData.IsWirelessIdleDisconnectEnabled = value; + this.OnPropertyChanged(nameof(IsWirelessIdleDisconnectEnabled)); + } + } + + public int WirelessIdleDisconnectTime + { + get => _tempBackingData.WirelessIdleDisconnectTime / 60000; + set + { + _tempBackingData.WirelessIdleDisconnectTime = value * 60000; + this.OnPropertyChanged(nameof(WirelessIdleDisconnectTime)); + } + } + + public ButtonComboViewModel QuickDisconnectButtonCombo { get; set; } + + public WirelessSettingsViewModel() : base() + { + QuickDisconnectButtonCombo = new(_tempBackingData.QuickDisconnectCombo, 0); + } + + public override void NotifyAllPropertiesHaveChanged() + { + base.NotifyAllPropertiesHaveChanged(); + QuickDisconnectButtonCombo.NotifyAllPropertiesChanged(); + } + } + + +} diff --git a/ControlApp/ViewModels/UserControls/DeviceSettingsEditorViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceSettingsEditorViewModel.cs new file mode 100644 index 00000000..c1d8e88c --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceSettingsEditorViewModel.cs @@ -0,0 +1,98 @@ +using System.ComponentModel; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; +using Nefarius.DsHidMini.ControlApp.ViewModels.UserControls.DeviceSettings; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.UserControls +{ + public partial class SettingsEditorViewModel : ObservableObject + { + private readonly List groupSettingsList = new(); + [ObservableProperty] public bool _allowEditing = false; + [ObservableProperty] private HidModeSettingsViewModel _hidModeVM = new(); + [ObservableProperty] private LedsSettingsViewModel _ledsSettingsVM = new(); + [ObservableProperty] private WirelessSettingsViewModel _wirelessSettingsVM = new(); + [ObservableProperty] private SticksSettingsViewModel _sticksSettingsVM = new(); + [ObservableProperty] private GeneralRumbleSettingsViewModel _generalRumbleSettingsVM = new(); + [ObservableProperty] private OutputReportSettingsViewModel _outRepSettingsVM = new(); + [ObservableProperty] private LeftMotorRescalingSettingsViewModel _leftMotorRescaleSettingsVM = new(); + [ObservableProperty] private AltRumbleModeSettingsViewModel _altRumbleSettingsVM = new(); + + public SettingsEditorViewModel() : this(null) + { + } + + public SettingsEditorViewModel(Models.DshmConfigManager.DeviceSettings? dataContainer = null) + { + groupSettingsList.Add(HidModeVM); + groupSettingsList.Add(LedsSettingsVM); + groupSettingsList.Add(WirelessSettingsVM); + groupSettingsList.Add(SticksSettingsVM); + groupSettingsList.Add(GeneralRumbleSettingsVM); + groupSettingsList.Add(OutRepSettingsVM); + groupSettingsList.Add(LeftMotorRescaleSettingsVM); + groupSettingsList.Add(AltRumbleSettingsVM); + + this.HidModeVM.PropertyChanged += ModeSettingsChanged; + + if (dataContainer != null) + LoadDatasToAllGroups(dataContainer); + + UpdateLockStateOfGroups(); + } + + private void UpdateLockStateOfGroups() + { + foreach (DeviceSettingsViewModel group in groupSettingsList) + { + group.IsGroupLocked = false; + } + + if (HidModeVM.Context == SettingsContext.DS4W) + { + SticksSettingsVM.IsGroupLocked = HidModeVM.PreventRemappingConflictsInDS4WMode; + } + + if (HidModeVM.Context == SettingsContext.SXS) + { + //SticksSettingsVM.IsGroupLocked = HidModeVM.PreventRemappingConflictsInSXSMode; + // Currently, OutReps being sent in SXS modes are passed-thru directly to the controller so + // Leds and rumble manipulations aren't applied + GeneralRumbleSettingsVM.IsGroupLocked = true; + LedsSettingsVM.IsGroupLocked = true; + LeftMotorRescaleSettingsVM.IsGroupLocked = true; + AltRumbleSettingsVM.IsGroupLocked = true; + } + } + + private void ModeSettingsChanged(object sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case "": + case nameof(HidModeSettingsViewModel.Context): + case nameof(HidModeSettingsViewModel.PreventRemappingConflictsInSXSMode): + case nameof(HidModeSettingsViewModel.PreventRemappingConflictsInDS4WMode): + UpdateLockStateOfGroups(); + break; + } + } + + public void SaveAllChangesToBackingData(Models.DshmConfigManager.DeviceSettings dataContainer) + { + foreach (DeviceSettingsViewModel group in groupSettingsList) + { + group.SaveSettingsToBackingDataContainer(dataContainer); + } + + } + + public void LoadDatasToAllGroups(Models.DshmConfigManager.DeviceSettings dataContainer) + { + foreach (DeviceSettingsViewModel group in groupSettingsList) + { + group.LoadSettingsFromBackingDataContainer(dataContainer); + } + + } + } +} diff --git a/ControlApp/ViewModels/UserControls/DeviceViewModel.cs b/ControlApp/ViewModels/UserControls/DeviceViewModel.cs new file mode 100644 index 00000000..2db11fff --- /dev/null +++ b/ControlApp/ViewModels/UserControls/DeviceViewModel.cs @@ -0,0 +1,488 @@ +using System.Net.NetworkInformation; +using Nefarius.DsHidMini.ControlApp.Models; +using Nefarius.DsHidMini.ControlApp.Models.Drivers; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.Enums; +using Nefarius.DsHidMini.ControlApp.Models.Enums; +using Nefarius.DsHidMini.ControlApp.Models.Util.Web; +using Nefarius.DsHidMini.ControlApp.Models.Util; +using Nefarius.DsHidMini.ControlApp.Services; +using Nefarius.DsHidMini.ControlApp.ViewModels.Pages; +using Nefarius.DsHidMini.ControlApp.ViewModels.UserControls; +using Nefarius.Utilities.Bluetooth; +using Nefarius.Utilities.DeviceManagement.PnP; + +using Serilog; + +using Wpf.Ui; +using Wpf.Ui.Controls; +using System.Text.RegularExpressions; +using Wpf.Ui.Extensions; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels +{ + public partial class DeviceViewModel : ObservableObject + { + // ------------------------------------------------------ FIELDS + + private readonly DshmConfigManager _dshmConfigManager; + private readonly AppSnackbarMessagesService _appSnackbarMessagesService; + private readonly IContentDialogService _contentDialogService; + private readonly PnPDevice _device; + private readonly DshmDevMan _dshmDevMan; + private readonly Timer _batteryQuery; + private DeviceData deviceUserData; + + public PnPDevice Device => _device; + + /// + /// Settings View Model for device's custom settings + /// Editing allowed, changes saved only if applying settings with custom settings mode selected + /// + [ObservableProperty] private SettingsEditorViewModel _deviceCustomsVM = new() { AllowEditing = true }; + + [ObservableProperty] private bool _isEditorEnabled; + + + /// + /// Determines if the settings editor is visible. + /// True if in custom mode, false otherwise + /// + [ObservableProperty] private bool _isEditorVisible; + + /// + /// Determines if the profile selector is visible. + /// True if in Global or Profile settings mode, false otherwise + /// + [ObservableProperty] private bool _isProfileSelectorVisible; + + /// + /// Determines if the profile selector is enabled. + /// True if in Profile settings mode, false otherwise + /// + [ObservableProperty] private bool _isProfileSelectorEnabled; + + /// + /// Desired settings mode for current device. Saved to device data only if applying settings + /// + [ObservableProperty] private SettingsModes _currentDeviceSettingsMode; + + /// + /// Current HID device emulation mode. + /// + public SettingsContext HidEmulationMode => DshmDriverTranslationUtils.HidDeviceMode[_device.GetProperty(DsHidMiniDriver.HidDeviceModeProperty)]; + + public HidModeShort HidModeShort => (HidModeShort)HidEmulationMode; + + /// + /// The Hid Mode the device is expected to be based on the device's user data + /// + public SettingsContext ExpectedHidMode => _dshmConfigManager.GetDeviceExpectedHidMode(deviceUserData); + + + /// + /// State of Device's current HID Mode in relation to mode it's expected to be + /// + public bool IsHidModeMismatched => (HidEmulationMode != ExpectedHidMode) ? true : false; + + /// + /// Summary of device's current HID mode and Settings mode + /// + public string DeviceSettingsStatus + { + get + { + string activeProfile = ""; + if (CurrentDeviceSettingsMode != SettingsModes.Custom) + { + switch (CurrentDeviceSettingsMode) + { + case SettingsModes.Global: + activeProfile = $"{_dshmConfigManager.GlobalProfile.ToString()}"; + break; + case SettingsModes.Profile: + activeProfile = $"{SelectedProfile.ToString()}"; + break; + default: break; + } + + activeProfile = $" 🡪 {activeProfile}"; + } + return $"{CurrentDeviceSettingsMode}{activeProfile}"; + } + } + + + /// + /// The friendly (product) name of this device. + /// + public string DisplayName + { + get + { + var name = _device.GetProperty(DevicePropertyKey.Device_FriendlyName); + + return string.IsNullOrEmpty(name) ? "DS3 Compatible HID Device" : name; + } + } + + + /// + /// The Bluetooth MAC address of this device. + /// + public string DeviceAddress => _device.GetProperty(DsHidMiniDriver.DeviceAddressProperty).ToUpper(); + + /// + /// The Bluetooth MAC address of this device. + /// + public string DeviceAddressFriendly + { + get + { + var friendlyAddress = DeviceAddress; + + var insertedCount = 0; + for (var i = 2; i < DeviceAddress.Length; i = i + 2) + friendlyAddress = friendlyAddress.Insert(i + insertedCount++, ":"); + + return friendlyAddress; + } + } + + + /// + /// The Bluetooth MAC address of the host radio this device is currently paired to. + /// + public string HostAddress + { + get + { + if (!WasLastHostRequestSuccessful) + { + return "Unkown"; + } + var hostAddress = _device.GetProperty(DsHidMiniDriver.HostAddressProperty).ToString("X12") + .ToUpper(); + + var friendlyAddress = hostAddress; + + var insertedCount = 0; + for (var i = 2; i < hostAddress.Length; i = i + 2) + friendlyAddress = friendlyAddress.Insert(i + insertedCount++, ":"); + + return friendlyAddress; + } + } + + + private string? _customPairingAddress = ""; + + /// + /// The Bluetooth MAC address of the host radio the controller should pair to if in custom pairing mode + /// + public string? CustomPairingAddress + { + get + { + var regex = "(.{2})(.{2})(.{2})(.{2})(.{2})(.{2})"; + var replace = "$1:$2:$3:$4:$5:$6"; + var newformat = Regex.Replace(_customPairingAddress, regex, replace); + return newformat; + } + set + { + var formattedCustomMacAddress = Regex.Replace(value, @"[^a-fA-F0-9]", string.Empty).ToUpper(); + if (formattedCustomMacAddress.Length > 12) + { + formattedCustomMacAddress = formattedCustomMacAddress.Substring(0, 12); + } + _customPairingAddress = formattedCustomMacAddress; + this.OnPropertyChanged(nameof(CustomPairingAddress)); + } + } + + /// + /// The desired Bluetooth pairing mode for the device when plugging via cable or applying settings + /// + private BluetoothPairingMode? _pairingMode; + + /// + /// Index of the desired Bluetooth pairing mode + /// + public int PairingMode + { + get => (int)_pairingMode; + set + { + _pairingMode = (BluetoothPairingMode)value; + this.OnPropertyChanged(nameof(PairingMode)); + } + + } + + + /// + /// Current battery status. + /// + public DsBatteryStatus BatteryStatus => + (DsBatteryStatus)_device.GetProperty(DsHidMiniDriver.BatteryStatusProperty); + + /// + /// String representation of current battery status + /// + public string BatteryStatusInText => + ((DsBatteryStatus)_device.GetProperty(DsHidMiniDriver.BatteryStatusProperty)).ToString(); + + /// + /// Return a battery icon depending on the charge. + /// + public SymbolRegular BatteryIcon + { + get + { + switch (BatteryStatus) + { + case DsBatteryStatus.Charged: + return SymbolRegular.Battery1024; + case DsBatteryStatus.Charging: + return SymbolRegular.BatteryCharge24; + case DsBatteryStatus.Full: + return SymbolRegular.Battery1024; + case DsBatteryStatus.High: + return SymbolRegular.Battery724; + case DsBatteryStatus.Medium: + return SymbolRegular.Battery524; + case DsBatteryStatus.Low: + return SymbolRegular.Battery224; + case DsBatteryStatus.Dying: + return SymbolRegular.Battery024; + default: + return SymbolRegular.BatteryWarning24; + } + } + } + + /// + /// Representation of last pairing attempt status + /// + public SymbolRegular LastPairingStatusIcon + { + get + { + var ntstatus = _device.GetProperty(DsHidMiniDriver.LastPairingStatusProperty); + return ( ntstatus == 0) + ? SymbolRegular.CheckmarkCircle24 + : SymbolRegular.DismissCircle24; + } + } + + /// + /// True if last host request was sucessful + /// + public bool WasLastHostRequestSuccessful => _device.GetProperty(DsHidMiniDriver.LastHostRequestStatusProperty) == 0; + + /// + /// Representation of genuine status of device + /// + //public SymbolRegular GenuineIcon + //{ + // get + // { + // // if (Validator.IsGenuineAddress(PhysicalAddress.Parse(DeviceAddress))) + // //return SymbolRegular.CheckmarkCircle24; + // //return SymbolRegular.ErrorCircle24; + // } + //} + + + /// + /// The wireless state of the device + /// + public bool IsWireless + { + get + { + var enumerator = _device.GetProperty(DevicePropertyKey.Device_EnumeratorName); + + return !enumerator.Equals("USB", StringComparison.InvariantCultureIgnoreCase); + } + } + + /// + /// Icon for connection protocol + /// + public SymbolRegular ConnectionTypeIcon => + !IsWireless + ? SymbolRegular.UsbPlug24 + : SymbolRegular.Bluetooth24; + + /// + /// Last time this device has been seen connected (applies to Bluetooth connected devices only). + /// + public DateTimeOffset LastConnected => + _device.GetProperty(DsHidMiniDriver.BluetoothLastConnectedTimeProperty); + + /// + /// The driver version of the device + /// + public string DriverVersion => _device.GetProperty(DevicePropertyKey.Device_DriverVersion).ToUpper(); + + /// + /// The device Instance ID. + /// + public string InstanceId => _device.InstanceId; + + private void UpdateBatteryStatus(object state) + { + OnPropertyChanged(nameof(BatteryStatus)); + } + + + + // ------------------------------------------------------ CONSTRUCTOR + + internal DeviceViewModel(PnPDevice device, DshmDevMan dshmDevMan, DshmConfigManager dshmConfigManager, AppSnackbarMessagesService appSnackbarMessagesService, IContentDialogService contentDialogService) + { + _device = device; + Log.Logger.Debug($"Creating Device ViewModel for device '{DeviceAddress}'"); + _dshmDevMan = dshmDevMan; + _dshmConfigManager = dshmConfigManager; + _appSnackbarMessagesService = appSnackbarMessagesService; + _contentDialogService = contentDialogService; + _batteryQuery = new Timer(UpdateBatteryStatus, null, 10000, 10000); + deviceUserData = _dshmConfigManager.GetDeviceData(DeviceAddress); + // Loads correspondent controller data based on controller's MAC address + + + //DisplayName = DeviceAddress; + RefreshDeviceSettings(); + } + + + // ------------------------------------------------------ METHODS + + [ObservableProperty] private ProfileData? _selectedProfile; + + [ObservableProperty] public List _listOfProfiles; + + partial void OnCurrentDeviceSettingsModeChanged(SettingsModes value) + { + AdjustSettingsTabState(); + } + + private void AdjustSettingsTabState() + { + IsProfileSelectorVisible = CurrentDeviceSettingsMode != SettingsModes.Custom; + IsProfileSelectorEnabled = CurrentDeviceSettingsMode == SettingsModes.Profile; + IsEditorVisible = CurrentDeviceSettingsMode == SettingsModes.Custom; + switch (CurrentDeviceSettingsMode) + { + case SettingsModes.Global: + SelectedProfile = _dshmConfigManager.GlobalProfile; + break; + case SettingsModes.Profile: + SelectedProfile = _dshmConfigManager.GetProfile(deviceUserData.GuidOfProfileToUse); + break; + default: + break; + } + } + + [RelayCommand] + public void RefreshDeviceSettings() + { + Log.Logger.Debug($"Refreshing ViewModel of Device '{DeviceAddress}'"); + // Bluetooth + PairingMode = (int)deviceUserData.BluetoothPairingMode; + CustomPairingAddress = deviceUserData.PairingAddress; + + // Settings and selected profile + CurrentDeviceSettingsMode = deviceUserData.SettingsMode; + DeviceCustomsVM.LoadDatasToAllGroups(deviceUserData.Settings); + ListOfProfiles = _dshmConfigManager.GetListOfProfilesWithDefault(); + + Log.Logger.Information($"Device '{DeviceAddress}' set for {ExpectedHidMode} HID Mode (currently in {HidModeShort}), {(BluetoothPairingMode)PairingMode} Bluetooth pairing mode."); + if ((BluetoothPairingMode)PairingMode == BluetoothPairingMode.Custom) + { + Log.Logger.Information($"Custom pairing address: {CustomPairingAddress}."); + } + AdjustSettingsTabState(); + this.OnPropertyChanged(nameof(DeviceSettingsStatus)); + this.OnPropertyChanged(nameof(IsHidModeMismatched)); + } + + [RelayCommand] + private void ApplyChanges() + { + Log.Logger.Information($"Saving and applying changes made to Device '{DeviceAddress}'"); + deviceUserData.BluetoothPairingMode = (BluetoothPairingMode)PairingMode; + + var formattedCustomMacAddress = Regex.Replace(CustomPairingAddress, @"[^a-fA-F0-9]", "").ToUpper(); + if(formattedCustomMacAddress.Length > 12) + { + formattedCustomMacAddress = formattedCustomMacAddress.Substring(0, 12); + } + deviceUserData.PairingAddress = formattedCustomMacAddress; + + deviceUserData.SettingsMode = CurrentDeviceSettingsMode; + if (CurrentDeviceSettingsMode == SettingsModes.Custom) + { + DeviceCustomsVM.SaveAllChangesToBackingData(deviceUserData.Settings); + } + + if (CurrentDeviceSettingsMode == SettingsModes.Profile) + { + deviceUserData.GuidOfProfileToUse = SelectedProfile.ProfileGuid; + } + + _dshmConfigManager.SaveChangesAndUpdateDsHidMiniConfigFile(); + _appSnackbarMessagesService.ShowDsHidMiniConfigurationUpdateSuccessMessage(); + RefreshDeviceSettings(); + } + + [RelayCommand] + private void RestartDevice() + { + bool reconnectionResult = _dshmDevMan.TryReconnectDevice(_device); + Log.Logger.Information($"User instructed {(IsWireless ? "wireless" : "wired")} device '{DeviceAddress}' to restart/disconnect."); + _appSnackbarMessagesService.ShowPowerCyclingDeviceMessage(IsWireless, Main.IsAdministrator(), reconnectionResult); + } + + [RelayCommand] + private async void TriggerPairingOnHotReload() + { + deviceUserData.BluetoothPairingMode = (BluetoothPairingMode)PairingMode; + var formattedCustomMacAddress = Regex.Replace(CustomPairingAddress, @"[^a-fA-F0-9]", "").ToUpper(); + if (formattedCustomMacAddress.Length > 12) + { + formattedCustomMacAddress = formattedCustomMacAddress.Substring(0, 12); + } + deviceUserData.PairingAddress = formattedCustomMacAddress; + + deviceUserData.PairOnHotReload = true; + _dshmConfigManager.SaveChangesAndUpdateDsHidMiniConfigFile(); + await ShowPairingDialog(); + deviceUserData.PairOnHotReload = false; + _dshmConfigManager.SaveChangesAndUpdateDsHidMiniConfigFile(); + this.OnPropertyChanged(nameof(HostAddress)); + this.OnPropertyChanged(nameof(LastPairingStatusIcon)); + } + + private async Task ShowPairingDialog() + { + var result = await _contentDialogService.ShowSimpleDialogAsync( + new SimpleContentDialogCreateOptions() + { + Title = "Manual pairing triggered", + Content = @"Pairing was requested. + +Wait 2 or 5 seconds before hitting ok to check for results.", + //PrimaryButtonText = "Ok", + //SecondaryButtonText = "Don't Save", + CloseButtonText = "OK", + } + ); + } + } + +} \ No newline at end of file diff --git a/ControlApp/ViewModels/UserControls/ProfileViewModel.cs b/ControlApp/ViewModels/UserControls/ProfileViewModel.cs new file mode 100644 index 00000000..496be363 --- /dev/null +++ b/ControlApp/ViewModels/UserControls/ProfileViewModel.cs @@ -0,0 +1,83 @@ +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Services; +using Nefarius.DsHidMini.ControlApp.ViewModels.UserControls; + +using Serilog; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels; + +public partial class ProfileViewModel : ObservableObject +{ + private readonly AppSnackbarMessagesService _appSnackbarMessagesService; + private readonly DshmConfigManager _dshmConfigManager; + + public ProfileData ProfileData { get; } + + [ObservableProperty] private string _name; + [ObservableProperty] private SettingsEditorViewModel _vmGroupsCont; + [ObservableProperty] private bool _isGlobal = false; + + public bool IsEditEnabled + { + get => _vmGroupsCont.AllowEditing; + private set + { + Log.Logger.Information($"Edition of profile '{ProfileData.ProfileName}' ({ProfileData.ProfileGuid}) set to {value}"); + _vmGroupsCont.AllowEditing = value; + this.OnPropertyChanged(nameof(IsEditEnabled)); + } + } + + + public ProfileViewModel(ProfileData data, AppSnackbarMessagesService appSnackbarMessagesService, DshmConfigManager dshmConfigManager) + { + _dshmConfigManager = dshmConfigManager; + _appSnackbarMessagesService = appSnackbarMessagesService; + ProfileData = data; + _name = data.ProfileName; + _vmGroupsCont = new(data.Settings); + } + + [RelayCommand] + public void EnableEditing() + { + if (ProfileData == ProfileData.DefaultProfile) + { + _appSnackbarMessagesService.ShowDefaultProfileEditingBlockedMessage(); + return; + } + IsEditEnabled = true; + } + + [RelayCommand] + public void SaveChanges() + { + Log.Logger.Information($"Saving changes to profile '{ProfileData.ProfileName}' ({ProfileData.ProfileGuid})"); + if(string.IsNullOrEmpty(_name)) + { + Log.Logger.Debug("New profile name is null or empty. Setting name to generic one."); + Name = "User Profile"; + } + + if (ProfileData.ProfileName != _name) + { + Log.Logger.Information($"Profile name changed from '{ProfileData.ProfileName}' to '{_name}'"); + ProfileData.ProfileName = _name; + } + + VmGroupsCont.SaveAllChangesToBackingData(ProfileData.Settings); + _dshmConfigManager.SaveChangesAndUpdateDsHidMiniConfigFile(); + IsEditEnabled = false; + + _appSnackbarMessagesService.ShowProfileUpdateMessage(); + } + + [RelayCommand] + public void CancelChanges() + { + Log.Logger.Debug($"Canceled changes to profile '{ProfileData.ProfileName}' ({ProfileData.ProfileGuid})"); + VmGroupsCont.LoadDatasToAllGroups(ProfileData.Settings); + Name = ProfileData.ProfileName; + IsEditEnabled = false; + } +} \ No newline at end of file diff --git a/ControlApp/ViewModels/Windows/MainWindowViewModel.cs b/ControlApp/ViewModels/Windows/MainWindowViewModel.cs new file mode 100644 index 00000000..e139f7f0 --- /dev/null +++ b/ControlApp/ViewModels/Windows/MainWindowViewModel.cs @@ -0,0 +1,142 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Collections.ObjectModel; +using Nefarius.DsHidMini.ControlApp.Models; +using Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager; +using Nefarius.DsHidMini.ControlApp.Services; +using Wpf.Ui; +using Wpf.Ui.Controls; +using static Nefarius.DsHidMini.ControlApp.Models.DshmConfigManager.DshmConfigManager; + +namespace Nefarius.DsHidMini.ControlApp.ViewModels.Windows +{ + public partial class MainWindowViewModel : ObservableObject + { + private readonly AppSnackbarMessagesService _appSnackbarMessagesService; + + [ObservableProperty] + private string _applicationTitle = "DsHidMini ControlApp"; + + [ObservableProperty] + private bool _IsAppElevated = Main.IsAdministrator(); + + [ObservableProperty] + private ObservableCollection _menuItems = new() + { + new NavigationViewItem() + { + Content = "Devices", + Icon = new SymbolIcon { Symbol = SymbolRegular.XboxController48 }, + TargetPageType = typeof(Views.Pages.DevicesPage) + }, + new NavigationViewItem() + { + Content = "Profiles", + Icon = new SymbolIcon { Symbol = SymbolRegular.DocumentOnePage20 }, + TargetPageType = typeof(Views.Pages.ProfilesPage) + }, + }; + + [ObservableProperty] + private ObservableCollection _footerMenuItems = new() + { + new NavigationViewItem() + { + Content = "Settings", + Icon = new SymbolIcon { Symbol = SymbolRegular.Settings24 }, + TargetPageType = typeof(Views.Pages.SettingsPage) + } + }; + + [ObservableProperty] + private ObservableCollection _trayMenuItems = new() + { + new MenuItem { Header = "Home", Tag = "tray_home" } + }; + + public MainWindowViewModel(AppSnackbarMessagesService appSnackbarMessagesService) + { + _appSnackbarMessagesService = appSnackbarMessagesService; + if (IsAppElevated) + { + ApplicationTitle += " [Administrador]"; + } + } + + public ApplicationConfiguration AppConfig => ApplicationConfiguration.Instance; + + [RelayCommand] + public void RestartAsAdmin() + { + Main.RestartAsAdmin(); + } + + + /* + /// + /// Helper to check if run with elevated privileges. + /// + public bool IsElevated => SecurityUtil.IsElevated; + + /// + /// True if run as regular, non-administrative user. + /// + public bool IsMissingPermissions => !IsElevated; + + /// + /// Is it possible to edit the selected device. + /// + public bool IsEditable => IsElevated && HasDeviceSelected && !IsRestarting; + + /// + /// Is the selected device in the process of getting restarted. + /// + public bool IsRestarting { get; set; } = false; + + /// + /// Version to display in window title. + /// + public string Version => $"Version: {Assembly.GetEntryAssembly().GetName().Version}"; + + + private static string ParametersKey => + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\WUDF\\Services\\dshidmini\\Parameters"; + + /// + /// Indicates if verbose logging is on or off. + /// + public bool VerboseOn + { + get + { + using (var key = RegistryHelpers.GetRegistryKey(ParametersKey)) + { + if (key == null) return false; + var value = key.GetValue("VerboseOn"); + return value != null && (int) value > 0; + } + } + set + { + using (var key = RegistryHelpers.GetRegistryKey(ParametersKey, true)) + { + key?.SetValue("VerboseOn", value ? 1 : 0, RegistryValueKind.DWord); + } + } + } + + public void RefreshProperties() + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsFilterEnabled")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsRawPDODisabled")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AreBthPS3SettingsIncorrect")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AreBthPS3SettingsCorrect")); + } + */ + + + } +} diff --git a/ControlApp/Views/Pages/DevicesPage.xaml b/ControlApp/Views/Pages/DevicesPage.xaml new file mode 100644 index 00000000..1074164c --- /dev/null +++ b/ControlApp/Views/Pages/DevicesPage.xaml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ControlApp/Views/Pages/DevicesPage.xaml.cs b/ControlApp/Views/Pages/DevicesPage.xaml.cs new file mode 100644 index 00000000..f30b3e11 --- /dev/null +++ b/ControlApp/Views/Pages/DevicesPage.xaml.cs @@ -0,0 +1,28 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using Nefarius.DsHidMini.ControlApp.ViewModels.Pages; +using Wpf.Ui.Controls; + +namespace Nefarius.DsHidMini.ControlApp.Views.Pages +{ + public partial class DevicesPage : INavigableView + { + public DevicesViewModel ViewModel { get; } + + public DevicesPage(DevicesViewModel viewModel) + { + ViewModel = viewModel; + DataContext = this; + + InitializeComponent(); + } + + private void ListView_Scroll(object sender, System.Windows.Controls.Primitives.ScrollEventArgs e) + { + + } + } +} diff --git a/ControlApp/Views/Pages/ProfilesPage.xaml b/ControlApp/Views/Pages/ProfilesPage.xaml new file mode 100644 index 00000000..234868a5 --- /dev/null +++ b/ControlApp/Views/Pages/ProfilesPage.xaml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ControlApp/Views/Pages/ProfilesPage.xaml.cs b/ControlApp/Views/Pages/ProfilesPage.xaml.cs new file mode 100644 index 00000000..1319900a --- /dev/null +++ b/ControlApp/Views/Pages/ProfilesPage.xaml.cs @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using Nefarius.DsHidMini.ControlApp.ViewModels.Pages; +using Wpf.Ui.Controls; + +namespace Nefarius.DsHidMini.ControlApp.Views.Pages +{ + public partial class ProfilesPage : INavigableView + { + public ProfilesViewModel ViewModel { get; } + + public ProfilesPage(ProfilesViewModel viewModel) + { + ViewModel = viewModel; + DataContext = this; + + InitializeComponent(); + } + } +} diff --git a/ControlApp/Views/Pages/SettingsPage.xaml b/ControlApp/Views/Pages/SettingsPage.xaml new file mode 100644 index 00000000..53dea52e --- /dev/null +++ b/ControlApp/Views/Pages/SettingsPage.xaml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/ControlApp/Views/Pages/SettingsPage.xaml.cs b/ControlApp/Views/Pages/SettingsPage.xaml.cs new file mode 100644 index 00000000..90d8cd65 --- /dev/null +++ b/ControlApp/Views/Pages/SettingsPage.xaml.cs @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using Nefarius.DsHidMini.ControlApp.ViewModels.Pages; +using Wpf.Ui.Controls; + +namespace Nefarius.DsHidMini.ControlApp.Views.Pages +{ + public partial class SettingsPage : INavigableView + { + public SettingsViewModel ViewModel { get; } + + public SettingsPage(SettingsViewModel viewModel) + { + ViewModel = viewModel; + DataContext = this; + + InitializeComponent(); + } + } +} diff --git a/ControlApp/Views/UserControls/DeviceSettingsEditor.xaml b/ControlApp/Views/UserControls/DeviceSettingsEditor.xaml new file mode 100644 index 00000000..0c846214 --- /dev/null +++ b/ControlApp/Views/UserControls/DeviceSettingsEditor.xaml @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ControlApp/Views/UserControls/DeviceSettingsEditor.xaml.cs b/ControlApp/Views/UserControls/DeviceSettingsEditor.xaml.cs new file mode 100644 index 00000000..7f719312 --- /dev/null +++ b/ControlApp/Views/UserControls/DeviceSettingsEditor.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Nefarius.DsHidMini.ControlApp.Views.UserControls +{ + /// + /// Interaction logic for DeviceSettingsEditor.xaml + /// + public partial class DeviceSettingsEditor : UserControl + { + public DeviceSettingsEditor() + { + InitializeComponent(); + } + } +} diff --git a/ControlApp/Views/UserControls/DeviceUserControl.xaml b/ControlApp/Views/UserControls/DeviceUserControl.xaml new file mode 100644 index 00000000..69afe02c --- /dev/null +++ b/ControlApp/Views/UserControls/DeviceUserControl.xaml @@ -0,0 +1,316 @@ + + + + 200 + + + 25 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ControlApp/Views/UserControls/DeviceUserControl.xaml.cs b/ControlApp/Views/UserControls/DeviceUserControl.xaml.cs new file mode 100644 index 00000000..ff9bdcfb --- /dev/null +++ b/ControlApp/Views/UserControls/DeviceUserControl.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Nefarius.DsHidMini.ControlApp.Views.UserControls +{ + /// + /// Interaction logic for DeviceSettingsEditor.xaml + /// + public partial class DeviceUserControl : UserControl + { + public DeviceUserControl() + { + InitializeComponent(); + } + } +} diff --git a/ControlApp/Views/UserControls/ProfileUserControl.xaml b/ControlApp/Views/UserControls/ProfileUserControl.xaml new file mode 100644 index 00000000..a9780e26 --- /dev/null +++ b/ControlApp/Views/UserControls/ProfileUserControl.xaml @@ -0,0 +1,62 @@ + + + + 200 + + + + + + + + + + + + + + + + + + + + + diff --git a/ControlApp/Views/UserControls/ProfileUserControl.xaml.cs b/ControlApp/Views/UserControls/ProfileUserControl.xaml.cs new file mode 100644 index 00000000..72020524 --- /dev/null +++ b/ControlApp/Views/UserControls/ProfileUserControl.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Nefarius.DsHidMini.ControlApp.Views.UserControls +{ + /// + /// Interaction logic for DeviceSettingsEditor.xaml + /// + public partial class ProfileUserControl : UserControl + { + public ProfileUserControl() + { + InitializeComponent(); + } + } +} diff --git a/ControlApp/Views/Windows/MainWindow.xaml b/ControlApp/Views/Windows/MainWindow.xaml new file mode 100644 index 00000000..955b0e5c --- /dev/null +++ b/ControlApp/Views/Windows/MainWindow.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ControlApp/Views/Windows/MainWindow.xaml.cs b/ControlApp/Views/Windows/MainWindow.xaml.cs new file mode 100644 index 00000000..12e92b2b --- /dev/null +++ b/ControlApp/Views/Windows/MainWindow.xaml.cs @@ -0,0 +1,101 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Nefarius.DsHidMini.ControlApp.Models; +using Nefarius.DsHidMini.ControlApp.Models.Drivers; +using Nefarius.DsHidMini.ControlApp.ViewModels.Windows; +using Nefarius.Utilities.DeviceManagement.PnP; + +using Wpf.Ui; +using Wpf.Ui.Appearance; +using Wpf.Ui.Controls; + +namespace Nefarius.DsHidMini.ControlApp.Views.Windows +{ + public partial class MainWindow : INavigationWindow + { + private readonly DshmDevMan _dshmDevMan; + public MainWindowViewModel ViewModel { get; } + + public MainWindow( + MainWindowViewModel viewModel, + DshmDevMan dshmDevMan, // + INavigationService navigationService, + IServiceProvider serviceProvider, + ISnackbarService snackbarService, + IContentDialogService contentDialogService + ) + { + + + ViewModel = viewModel; + DataContext = this; + + _dshmDevMan = dshmDevMan; + + Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this); + + ApplicationThemeManager.Apply(ApplicationTheme.Dark); + + InitializeComponent(); + + navigationService.SetNavigationControl(RootNavigation); + snackbarService.SetSnackbarPresenter(SnackbarPresenter); + contentDialogService.SetContentPresenter(RootContentDialog); + } + + protected override void OnSourceInitialized(EventArgs e) + { + base.OnSourceInitialized(e); + + InitializeComponent(); + _dshmDevMan.StartListeningForDshmDevices(); + } + + protected override void OnClosing(CancelEventArgs e) + { + base.OnClosing(e); + + _dshmDevMan.StopListeningForDshmDevices(); + } + + + #region INavigationWindow methods + + public INavigationView GetNavigation() + { + return RootNavigation; + } + + public bool Navigate(Type pageType) + { + return RootNavigation.Navigate(pageType); + } + + public void SetPageService(IPageService pageService) + { + RootNavigation.SetPageService(pageService); + } + + public void ShowWindow() + { + Show(); + } + + public void CloseWindow() + { + Close(); + } + + public void SetServiceProvider(IServiceProvider serviceProvider) + { + throw new NotImplementedException(); + } + + #endregion INavigationWindow methods + } +} \ No newline at end of file diff --git a/ControlApp/app.manifest b/ControlApp/app.manifest new file mode 100644 index 00000000..51c5d99f --- /dev/null +++ b/ControlApp/app.manifest @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PerMonitor + true/PM + true + + + + + + + + + + diff --git a/ControlApp/wpfui-icon.ico b/ControlApp/wpfui-icon.ico new file mode 100644 index 00000000..cc128fda Binary files /dev/null and b/ControlApp/wpfui-icon.ico differ diff --git a/XInputBridge/XInputBridge.vcxproj b/XInputBridge/XInputBridge.vcxproj index 04f6ee76..e297a1ae 100644 --- a/XInputBridge/XInputBridge.vcxproj +++ b/XInputBridge/XInputBridge.vcxproj @@ -203,25 +203,25 @@ false XInput1_3 $(SolutionDir)bin\$(PlatformShortName)\ - true + true false XInput1_3 $(SolutionDir)bin\$(PlatformShortName)\OTEL\ - true + true false XInput1_3 $(SolutionDir)bin\$(PlatformShortName)\ - true + true false XInput1_3 $(SolutionDir)bin\$(PlatformShortName)\OTEL\ - true + true true @@ -236,13 +236,13 @@ false XInput1_3 $(SolutionDir)bin\$(PlatformShortName)\ - true + true false XInput1_3 $(SolutionDir)bin\$(PlatformShortName)\OTEL\ - true + true true diff --git a/appveyor.yml b/appveyor.yml index b8d5975c..339e1eca 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -46,6 +46,7 @@ before_build: - cmd: vpatch.exe --stamp-version "%APPVEYOR_BUILD_VERSION%" --target-file ".\XInputBridge\XInputBridge.rc" --resource.file-version --resource.product-version build_script: - cmd: .\build.cmd +- cmd: if %PLATFORM%==x64 dotnet publish /p:PublishProfile=Properties\PublishProfiles\release-win-x64.pubxml .\ControlApp\ after_build: - cmd: if not %PLATFORM%==x86 makecab.exe /f .\DsHidMini_%PLATFORM%.ddf artifacts: diff --git a/dshidmini.sln b/dshidmini.sln index a08f9314..349939a7 100644 --- a/dshidmini.sln +++ b/dshidmini.sln @@ -37,6 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Setup", "Setup", "{58E023F1 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DsHidMini.Installer", "setup\DsHidMini.Installer.csproj", "{7D07C49F-A5A8-44AD-83B5-1E5B1F3080AA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlApp", "ControlApp\ControlApp.csproj", "{AD47E724-2038-46EA-ACF9-C28B53D39A9A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug_OTEL|Any CPU = Debug_OTEL|Any CPU @@ -130,13 +132,9 @@ Global {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release_OTEL|x64.ActiveCfg = Release|Any CPU {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release_OTEL|x86.ActiveCfg = Release|Any CPU {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release|Any CPU.Build.0 = Release|Any CPU {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release|ARM64.ActiveCfg = Release|Any CPU - {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release|ARM64.Build.0 = Release|Any CPU {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release|x64.ActiveCfg = Release|Any CPU - {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release|x64.Build.0 = Release|Any CPU {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release|x86.ActiveCfg = Release|Any CPU - {8C041739-B65C-4BE3-B068-B13ED698C72E}.Release|x86.Build.0 = Release|Any CPU {DC58FE95-938F-4BFB-B434-C043C62E9EF5}.Debug_OTEL|Any CPU.ActiveCfg = Debug_OTEL|x64 {DC58FE95-938F-4BFB-B434-C043C62E9EF5}.Debug_OTEL|Any CPU.Build.0 = Debug_OTEL|x64 {DC58FE95-938F-4BFB-B434-C043C62E9EF5}.Debug_OTEL|ARM64.ActiveCfg = Debug_OTEL|ARM64 @@ -221,6 +219,38 @@ Global {7D07C49F-A5A8-44AD-83B5-1E5B1F3080AA}.Release|ARM64.ActiveCfg = Release|Any CPU {7D07C49F-A5A8-44AD-83B5-1E5B1F3080AA}.Release|x64.ActiveCfg = Release|Any CPU {7D07C49F-A5A8-44AD-83B5-1E5B1F3080AA}.Release|x86.ActiveCfg = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug_OTEL|Any CPU.ActiveCfg = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug_OTEL|Any CPU.Build.0 = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug_OTEL|ARM64.ActiveCfg = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug_OTEL|ARM64.Build.0 = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug_OTEL|x64.ActiveCfg = Debug|x64 + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug_OTEL|x64.Build.0 = Debug|x64 + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug_OTEL|x86.ActiveCfg = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug_OTEL|x86.Build.0 = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug|ARM64.Build.0 = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug|x64.ActiveCfg = Debug|x64 + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug|x64.Build.0 = Debug|x64 + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug|x86.ActiveCfg = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Debug|x86.Build.0 = Debug|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release_OTEL|Any CPU.ActiveCfg = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release_OTEL|Any CPU.Build.0 = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release_OTEL|ARM64.ActiveCfg = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release_OTEL|ARM64.Build.0 = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release_OTEL|x64.ActiveCfg = Release|x64 + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release_OTEL|x64.Build.0 = Release|x64 + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release_OTEL|x86.ActiveCfg = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release_OTEL|x86.Build.0 = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|Any CPU.Build.0 = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|ARM64.ActiveCfg = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|ARM64.Build.0 = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|x64.ActiveCfg = Release|x64 + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|x64.Build.0 = Release|x64 + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|x86.ActiveCfg = Release|Any CPU + {AD47E724-2038-46EA-ACF9-C28B53D39A9A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -231,6 +261,7 @@ Global {DC58FE95-938F-4BFB-B434-C043C62E9EF5} = {D5FFDFFE-55A3-4AF3-92A6-13F28598A5EE} {FEB89FC0-BF9B-43F3-8467-B3496D61B76E} = {D5FFDFFE-55A3-4AF3-92A6-13F28598A5EE} {7D07C49F-A5A8-44AD-83B5-1E5B1F3080AA} = {58E023F1-01BB-4D75-90A5-2E6049F94048} + {AD47E724-2038-46EA-ACF9-C28B53D39A9A} = {CE492389-7FB3-4DC4-9AFF-B7A04F70F891} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5A4A580B-8A5D-4E6A-A75D-ADFDD46F6F37}