diff --git a/ColorControl.UI/ColorControl.UI.csproj b/ColorControl.UI/ColorControl.UI.csproj index a06ae38..e2a26dd 100644 --- a/ColorControl.UI/ColorControl.UI.csproj +++ b/ColorControl.UI/ColorControl.UI.csproj @@ -1,14 +1,15 @@  - net8.0-windows10.0.20348.0 + net9.0-windows enable enable - Library + Library + None - + diff --git a/ColorControl.UI/Components/Pages/ColorProfile/ColorProfilePage.razor b/ColorControl.UI/Components/Pages/ColorProfile/ColorProfilePage.razor index f2f1619..8f3ffbf 100644 --- a/ColorControl.UI/Components/Pages/ColorProfile/ColorProfilePage.razor +++ b/ColorControl.UI/Components/Pages/ColorProfile/ColorProfilePage.razor @@ -251,6 +251,17 @@ } + else + { +
+
+
+ + +
+
+
+ } diff --git a/ColorControl.UI/Components/Pages/ColorProfile/ColorProfileSummary.razor b/ColorControl.UI/Components/Pages/ColorProfile/ColorProfileSummary.razor index fd40bf2..8de5067 100644 --- a/ColorControl.UI/Components/Pages/ColorProfile/ColorProfileSummary.razor +++ b/ColorControl.UI/Components/Pages/ColorProfile/ColorProfileSummary.razor @@ -33,12 +33,12 @@ else { No displays found } - @foreach (var display in Displays) + else {
-
+
+ +
+ + + +
+
} @code { - [Parameter] - public string? Label { get; set; } + [Parameter] + public string? Label { get; set; } - [Parameter] - public string? Shortcut { get; set; } + [Parameter] + public string? Shortcut { get; set; } - [Parameter] - public EventCallback ShortcutChanged { get; set; } + [Parameter] + public EventCallback ShortcutChanged { get; set; } - private async Task ShortcutOnChange(ChangeEventArgs e) - { - Shortcut = e.Value?.ToString(); - await ShortcutChanged.InvokeAsync(Shortcut); - } + private Dictionary Modifiers = new() { + { "Shift", false }, + { "Control", false }, + { "Alt", false }, + { "Win", false } + }; - private void ShortcutOnKeyDown(KeyboardEventArgs e) - { - if (e.Key == "Tab") - { - return; - } + protected override void OnParametersSet() + { + LoadModifiers(); + } - var text = KeyboardShortcutDispatcher.FormatKeyboardShortcut(e.ShiftKey, e.CtrlKey, e.AltKey, e.Key, e.Code); - Shortcut = text; - } + private async Task ShortcutOnChange(ChangeEventArgs e) + { + Shortcut = e.Value?.ToString(); + LoadModifiers(); + await ShortcutChanged.InvokeAsync(Shortcut); + } - private async Task ShortcutOnKeyUp(KeyboardEventArgs e) - { - //KeyboardShortcutDispatcher.HandleKeyboardShortcutUp() - await ShortcutChanged.InvokeAsync(Shortcut); - } + private void ShortcutOnKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Tab") + { + return; + } - private void ShortcutOnFocus(FocusEventArgs e) - { - KeyboardShortcutDispatcher.IsActive = false; - } + var text = KeyboardShortcutDispatcher.FormatKeyboardShortcut(e.ShiftKey, e.CtrlKey, e.AltKey, e.Key, e.Code); + Shortcut = text; + } - private void ShortcutOnBlur(FocusEventArgs e) - { - KeyboardShortcutDispatcher.IsActive = true; - } + private async Task ShortcutOnKeyUp(KeyboardEventArgs e) + { + //KeyboardShortcutDispatcher.HandleKeyboardShortcutUp() + await ShortcutChanged.InvokeAsync(Shortcut); + } + + private void ShortcutOnFocus(FocusEventArgs e) + { + KeyboardShortcutDispatcher.IsActive = false; + } + + private void ShortcutOnBlur(FocusEventArgs e) + { + KeyboardShortcutDispatcher.IsActive = true; + } + + private void LoadModifiers() + { + foreach (var modifier in Modifiers.Keys) + { + Modifiers[modifier] = Shortcut?.Contains(modifier) == true; + } + } + private async Task ModifierKeyChanged(ChangeEventArgs e, string modifier) + { + var value = e.Value is bool boolValue && boolValue; + + Modifiers[modifier] = value; + + await SetModifiers(); + } + + private async Task SetModifiers() + { + var shortcut = Shortcut ?? ""; + foreach (var modifier in Modifiers.Keys) + { + shortcut = shortcut.Replace(modifier, "").Trim().TrimStart(','); + } + + shortcut = shortcut.Trim(); + + foreach (var modifier in Modifiers.Where(kv => kv.Value).Select(kv => kv.Key).Reverse()) + { + if (shortcut.StartsWith("+")) + { + shortcut = $"{modifier} {shortcut}"; + } + else + { + shortcut = $"{modifier}, {shortcut}"; + } + } + + Shortcut = shortcut; + + await ShortcutChanged.InvokeAsync(Shortcut); + } + + private async Task ModifierOnClick(string modifier) + { + Modifiers[modifier] = !Modifiers[modifier]; + + await SetModifiers(); + } } \ No newline at end of file diff --git a/ColorControl.UI/Components/Pages/Nvidia/NvidiaDithering.razor b/ColorControl.UI/Components/Pages/Nvidia/NvidiaDithering.razor index 79bf337..097a4c9 100644 --- a/ColorControl.UI/Components/Pages/Nvidia/NvidiaDithering.razor +++ b/ColorControl.UI/Components/Pages/Nvidia/NvidiaDithering.razor @@ -89,8 +89,8 @@ diff --git a/ColorControl.UI/Components/Pages/Nvidia/NvidiaPreset.razor b/ColorControl.UI/Components/Pages/Nvidia/NvidiaPreset.razor index b0beff5..57680b3 100644 --- a/ColorControl.UI/Components/Pages/Nvidia/NvidiaPreset.razor +++ b/ColorControl.UI/Components/Pages/Nvidia/NvidiaPreset.razor @@ -11,6 +11,7 @@ @inject RpcUiClientService _rpcClientService @inject AppState AppState @inject NavigationManager _navigationManager; +@inject JSHelper jsHelper; @@ -667,12 +668,20 @@ else { var result = await _rpcClientService.CallAsync("NvService", "UpdatePreset", Preset); + + if (!result) + { + return; + } } if (AfterApply != null) { await AfterApply.Invoke(Preset); } + + await jsHelper.CloseModal("presetModal"); + IsVisible = false; } private async Task ApplyDisplayPreset() diff --git a/ColorControl.UI/Components/Pages/Nvidia/NvidiaSummary.razor b/ColorControl.UI/Components/Pages/Nvidia/NvidiaSummary.razor index f93f204..703c642 100644 --- a/ColorControl.UI/Components/Pages/Nvidia/NvidiaSummary.razor +++ b/ColorControl.UI/Components/Pages/Nvidia/NvidiaSummary.razor @@ -90,6 +90,26 @@ else
Color profile: @preset.ColorProfileSettings.ProfileName } + @if (preset.driverSettings?.Any() == true) + { +
+ + } @@ -258,6 +278,7 @@ else private NvPreset? CurrentGpuSettingsPreset; private NvGpuInfoDto? CurrentGpuInfo; private bool DitheringVisible; + private List? DriverSettings; private PresetOrder PresetOrder; @@ -371,4 +392,14 @@ else Presets = Presets?.OrderPresetsBy(PresetOrder); } + + private async Task DriverSettingsOnClick(NvPreset preset) + { + var driverSettings = await _rpcClientService.CallAsync>("NvService", "GetDriverSettings", Type.Missing, true); + + if (driverSettings != null) + { + DriverSettings = driverSettings.Where(s => s.Value != s.DefaultValue && s.State == 2).ToList(); + } + } } diff --git a/ColorControl.UI/Components/Pages/Options.razor b/ColorControl.UI/Components/Pages/Options.razor index fbde3ad..f20374f 100644 --- a/ColorControl.UI/Components/Pages/Options.razor +++ b/ColorControl.UI/Components/Pages/Options.razor @@ -301,7 +301,7 @@ else private async Task ElevationMethodInput(ChangeEventArgs e) { - var value = e.Value != null ? (ElevationMethod)int.Parse((string)e.Value) : ElevationMethod.None; + var value = e.Value != null ? Enum.Parse((string)e.Value) : ElevationMethod.None; await _rpcClientService.CallAsync("OptionsService", "SetElevationMethod", value); } diff --git a/ColorControl/ColorControl.csproj b/ColorControl/ColorControl.csproj index 182843d..79dcf8c 100644 --- a/ColorControl/ColorControl.csproj +++ b/ColorControl/ColorControl.csproj @@ -1,6 +1,6 @@  - net8.0-windows10.0.20348.0 + net9.0-windows WinExe false publish\ @@ -18,8 +18,8 @@ Maassoft Maassoft 0 - 10.1.0.0 - 10.1.0.0 + 10.2.0.0 + 10.2.0.0 false true false @@ -28,8 +28,7 @@ false true Resources\pngbarn.ico - 10.0.20348.0 - 2020-2024 Maassoft + 2020-2025 Maassoft Maassoft CF0BC153EA664CD97AC6F234B91F45D34630958F ColorControl_1_TemporaryKey.pfx @@ -39,6 +38,7 @@ true ColorControl_TemporaryKey.pfx false + None bin\x64\Debug\ @@ -106,17 +106,15 @@
- - - + + + - - - - - - - + + + + + diff --git a/ColorControl/LgTv/LgTvConnectionCws.cs b/ColorControl/LgTv/LgTvConnectionCws.cs new file mode 100644 index 0000000..19f2b1a --- /dev/null +++ b/ColorControl/LgTv/LgTvConnectionCws.cs @@ -0,0 +1,297 @@ +using ColorControl.Shared.Common; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Concurrent; +using System.Net.Security; +using System.Net.WebSockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LgTv +{ + public class SendMessageExceptionCws : Exception + { + public SendMessageExceptionCws(string message, Exception e) : base(message, e) + { + + } + } + + public delegate void IsConnectedDelegateCws(bool status); + + public class LgTvApiCoreCws : IAsyncDisposable + { + public bool ConnectionClosed { get; private set; } + + private ClientWebSocket _clientWebSocket; + private int _commandCount; + private Uri _uri; + + public event IsConnectedDelegateCws IsConnected; + private readonly ConcurrentDictionary> _tokens = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _callbacks = new ConcurrentDictionary>(); + + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + public async Task Connect(Uri uri, bool ignoreReceiver = false) + { + _uri = uri; + + try + { + ConnectionClosed = false; + _commandCount = 0; + CreateWebSocket(); + + try + { + await _clientWebSocket.ConnectAsync(uri, CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(5000)); + ConnectionClosed = false; + + var _ = Receive(false); + + IsConnected?.Invoke(true); + return true; + } + catch (TimeoutException te) + { + Logger.Error($"Timeout while connecting to {uri}: {te.Message}"); + return false; + } + } + catch (Exception e) + { + Logger.Error($"Connect to {uri}: {e.Message}"); + return false; + } + } + + private void CreateWebSocket() + { + _clientWebSocket = new ClientWebSocket(); + _clientWebSocket.Options.RemoteCertificateValidationCallback += CertificationValidationCallback; + } + + private bool CertificationValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors) => true; + + public async Task SendMessageAsync(string message, bool reconnect = true) + { + try + { + if (_clientWebSocket?.State != WebSocketState.Open) + { + throw new InvalidOperationException("Socket not open"); + } + var bytes = UTF8Encoding.UTF8.GetBytes(message); + var data = bytes.AsMemory(); + + await _clientWebSocket.SendAsync(data, WebSocketMessageType.Text, true, CancellationToken.None); + } + catch (WebSocketException) + { + if (reconnect && await Connect(_uri)) + { + await SendMessageAsync(message, false); + } + } + catch (Exception e) + { + Logger.Error("SendMessageAsync: " + e.Message); + ConnectionClosed = true; + } + } + + public async Task SendCommandAsync(string message) + { + var obj = JsonConvert.DeserializeObject(message); + return await SendCommandAsync((string)obj.id, message); + } + + private async Task SendCommandAsync(string id, string message) + { + try + { + var taskSource = new TaskCompletionSource(); + _tokens.TryAdd(id, taskSource); + await SendMessageAsync(message); + if (ConnectionClosed) + { + throw new Exception("Connection closed"); + } + + return await taskSource.Task; + } + catch (Exception e) + { + throw new SendMessageExceptionCws("Can't send message", e); + } + } + + public async Task SendCommandAsync(RequestMessage message) + { + var rawMessage = new RawRequestMessage(message, ++_commandCount); + var serialized = JsonConvert.SerializeObject(rawMessage, new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + return await SendCommandAsync(rawMessage.Id, serialized); + } + + public async Task SubscribeAsync(RequestMessage message, Func callback) + { + var rawMessage = new RawRequestMessage(message, ++_commandCount); + var serialized = JsonConvert.SerializeObject(rawMessage, new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + _callbacks.TryAdd(rawMessage.Id, callback); + + return await SendCommandAsync(rawMessage.Id, serialized); + } + + private async Task Receive(bool singleResponse) + { + var dataPerPacket = 8 * 1024; //8192 + var strBuilder = new StringBuilder(); + var buffer = new ArraySegment(new byte[dataPerPacket]); + + try + { + while (_clientWebSocket?.State == WebSocketState.Open) + { + var receiveResult = await _clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); + + strBuilder.Append(Encoding.UTF8.GetString(buffer.Array, buffer.Offset, receiveResult.Count)); + //keep doing until end of message + while (!receiveResult.EndOfMessage) + { + receiveResult = await _clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); + strBuilder.Append(Encoding.UTF8.GetString(buffer.Array, buffer.Offset, receiveResult.Count)); + } + + var message = strBuilder.ToString(); + strBuilder.Clear(); + + HandleMessage(message); + + if (singleResponse) + { + return; + } + } + } + catch (Exception) + { + ConnectionClosed = true; + Logger.Debug("Connection closed"); + IsConnected?.Invoke(false); + } + } + + public void HandleMessage(string message) + { + try + { + var obj = JsonConvert.DeserializeObject(message); + var id = (string)obj.id; + var type = (string)obj.type; + + TaskCompletionSource taskCompletion; + if (type == "registered") + { + if (_tokens.TryRemove(id, out taskCompletion)) + { + var key = (string)JObject.Parse(message)["payload"]["client-key"]; + taskCompletion.TrySetResult(new { clientKey = key }); + } + + } + else if (_tokens.TryGetValue(id, out taskCompletion)) + { + if (id == "register_0") + { + //taskCompletion.TrySetResult(obj.payload); + return; + } + if (obj.type == "error") + { + taskCompletion.SetException(new Exception(obj.error?.ToString())); + } + //else if (args.Cancelled) + //{ + // taskSource.SetCanceled(); + //} + taskCompletion.TrySetResult(obj.payload); + + if (_callbacks.TryGetValue(id, out Func callback)) + { + try + { + callback(obj.payload); + } + catch (Exception callbackException) + { + Logger.Error($"Connection_MessageReceived: the callback threw an exception: {callbackException.ToLogString()}"); + } + } + } + } + catch (Exception ex) + { + Logger.Error($"HandleMessage: status: {_clientWebSocket.State}, exception: {ex.ToLogString()}"); + + SetExceptionOnAllTokens(ex); + + ConnectionClosed = true; + } + } + + public async ValueTask DisposeAsync() + { + if (_clientWebSocket != null) + { + if (_clientWebSocket.State == WebSocketState.Open) + { + try + { + await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + } + catch { } + } + _clientWebSocket?.Dispose(); + } + + GC.SuppressFinalize(this); + } + + private void SetExceptionOnAllTokens(Exception ex) + { + //Logger.Debug($"Tokens: {_tokens.Count}"); + + foreach (var taskCompletion in _tokens.Values) + { + try + { + if (taskCompletion.Task.Status != TaskStatus.RanToCompletion) + { + Logger.Debug("taskCompletion.Task.Status: " + taskCompletion.Task.Status); + taskCompletion.SetException(new Exception(ex.Message)); + } + } + catch (Exception ex2) + { + Logger.Error("SetExceptionOnAllTokens: " + ex2.Message); + } + } + + _tokens.Clear(); + } + } +} diff --git a/ColorControl/Services/AMD/AmdService.cs b/ColorControl/Services/AMD/AmdService.cs index 4880e10..ee45642 100644 --- a/ColorControl/Services/AMD/AmdService.cs +++ b/ColorControl/Services/AMD/AmdService.cs @@ -16,547 +16,549 @@ using System.Windows.Forms; namespace ColorControl.Services.AMD { - class AmdService : GraphicsService - { - private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - - public override string ServiceName => "AMD"; - - protected override string PresetsBaseFilename => "AmdPresets.json"; - - private ADLDisplayInfo _currentDisplay; - - public static readonly int SHORTCUTID_AMDQA = -201; - - private string _configFilename; - public AmdServiceConfig Config { get; private set; } - - public AmdService(GlobalContext globalContext) : base(globalContext) - { - LoadPresets(); - LoadConfig(); - } - - protected override void AfterPresetsLoaded() - { - // Force disable display preset on presets - _presets.ForEach(p => p.IsDisplayPreset = false); - } - - public static async Task ExecutePresetAsync(string idOrName) - { - try - { - var globalContext = Program.ServiceProvider.GetRequiredService(); - - var service = new AmdService(globalContext); - - var result = await service.ApplyPreset(idOrName); - - if (!result) - { - Console.WriteLine("Preset not found or error while executing."); - } - - return result; - } - catch (Exception ex) - { - Console.WriteLine("Error executing preset: " + ex.ToLogString()); - return false; - } - } - - public override void InstallEventHandlers() - { - base.InstallEventHandlers(); - - SetShortcuts(SHORTCUTID_AMDQA, Config.QuickAccessShortcut); - } - - private void LoadConfig() - { - _configFilename = Path.Combine(_dataPath, "AmdConfig.json"); - try - { - if (File.Exists(_configFilename)) - { - Config = JsonConvert.DeserializeObject(File.ReadAllText(_configFilename)); - } - } - catch (Exception ex) - { - Logger.Error($"LoadConfig: {ex.Message}"); - } - Config ??= new AmdServiceConfig(); - } - - protected override List GetDefaultPresets() - { - return AmdPreset.GetDefaultPresets(); - } - - public override List GetInfo() - { - return [$"{_presets.Count} presets"]; - } - - public void GlobalSave() - { - SavePresets(); - SaveConfig(); - } - - public override bool HasDisplaysAttached(bool reinitialize = false) - { - try - { - return GetDisplays().Any(); - } - catch (Exception) - { - return false; - } - } - - public void SetCurrentDisplay(AmdPreset preset) - { - var displays = GetDisplays(); - if (preset.primaryDisplay) - { - _currentDisplay = displays.FirstOrDefault(); - } - else - { - _currentDisplay = displays.FirstOrDefault(x => x.DisplayName.Equals(preset.displayName)); - } - } - - public ADLDisplayInfo GetCurrentDisplay() - { - if (string.IsNullOrEmpty(_currentDisplay.DisplayName)) - { - _currentDisplay = GetDisplays().FirstOrDefault(); - } - - return _currentDisplay; - } - - public override Task ApplyPreset(AmdPreset preset) - { - SetCurrentDisplay(preset); - - if (string.IsNullOrEmpty(_currentDisplay.DisplayName)) - { - Logger.Error("Could not set current display"); - return Task.FromResult(false); - } - - var result = true; - - if (preset.applyHDR) - { - var hdrEnabled = IsHDREnabled(); - if (preset.toggleHDR) - { - result = ADLWrapper.SetDisplayHDRState(_currentDisplay, !hdrEnabled); - } - else - { - result = ADLWrapper.SetDisplayHDRState(_currentDisplay, preset.HDREnabled); - } - - //if (preset.toggleHDR || preset.HDREnabled != hdrEnabled) - //{ - // ToggleHDR(config.DisplaySettingsDelay); - //} - - if (preset.SDRBrightness.HasValue) - { - var deviceName = GetDisplayDeviceName(_currentDisplay); - - SetSDRBrightness(deviceName, preset.SDRBrightness.Value); - } - } - - if (preset.DisplayConfig.ApplyRefreshRate || preset.DisplayConfig.ApplyResolution) - { - if (!SetMode(preset.DisplayConfig, true)) - { - result = false; - } - } - - if (preset.applyColorData) - { - if (!ADLWrapper.SetDisplayColorDepth(_currentDisplay, preset.colorDepth)) - { - Logger.Error("Could not set color depth"); - result = false; - } - if (preset.pixelFormat != ADLPixelFormat.UNKNOWN && !ADLWrapper.SetDisplayPixelFormat(_currentDisplay, preset.pixelFormat)) - { - Logger.Error("Could not set pixel format"); - result = false; - } - } - - if (preset.applyDithering) - { - if (!SetDithering(preset.ditherState)) - { - result = false; - } - } - - _lastAppliedPreset = preset; - - PresetApplied(); - - return Task.FromResult(result); - } - - public bool SetDithering(ADLDitherState ditherState) - { - var display = GetCurrentDisplay(); - - return ADLWrapper.SetDisplayDitherState(display, ditherState); - } - - public ADLDitherState GetDithering(ADLDisplayInfo display = default(ADLDisplayInfo)) - { - if (string.IsNullOrEmpty(display.DisplayName)) - { - display = GetCurrentDisplay(); - } - - var ditherState = ADLDitherState.DRIVER_DEFAULT; - - if (ADLWrapper.GetDisplayDitherState(display, ref ditherState)) - { - return ditherState; - } - - return ditherState; - } - - public string GetDisplayDeviceName(ADLDisplayInfo display) - { - return Screen.PrimaryScreen.DeviceName; - } - - public bool SetMode(DisplayConfig displayConfig, bool updateRegistry = false) - { - var display = GetCurrentDisplay(); - - return SetMode(GetDisplayDeviceName(display), displayConfig, updateRegistry); - } - - public List GetAvailableRefreshRates(AmdPreset preset = null) - { - if (preset != null) - { - SetCurrentDisplay(preset); - } - - var horizontal = 3840; - var vertical = 2160; - var display = GetCurrentDisplay(); - - ADLWrapper.GetDisplayResolution(display, ref horizontal, ref vertical); - - return GetAvailableRefreshRatesV2(GetDisplayDeviceName(display), horizontal, vertical); - } - - public List GetAvailableResolutionsV2(AmdPreset preset = null) - { - if (preset != null) - { - SetCurrentDisplay(preset); - } - - var display = GetCurrentDisplay(); - var deviceName = GetDisplayDeviceName(display); - if (deviceName == null) - { - return []; - } - - return GetAvailableResolutionsInternalV2(deviceName); - } - - public bool SetMode(VirtualResolution resolution = null, Rational refreshRate = null, bool updateRegistry = false, AmdPreset preset = null) - { - var display = preset == null ? GetCurrentDisplay() : GetPresetDisplay(preset); - if (display.Equals(default(ADLDisplayInfo))) - { - return false; - } - - return SetMode(GetDisplayDeviceName(display), resolution, refreshRate, updateRegistry); - } - - public bool SetMode(DisplayConfig displayConfig, bool updateRegistry = false, AmdPreset preset = null) - { - var display = preset == null ? GetCurrentDisplay() : GetPresetDisplay(preset); - if (display.DisplayID.DisplayPhysicalIndex == 0) - { - return false; - } - - return SetMode(GetDisplayDeviceName(display), displayConfig, updateRegistry); - } - - public ADLDisplayInfo GetPresetDisplay(AmdPreset preset) - { - return GetCurrentDisplay(); - } - - public List GetAvailableRefreshRatesV2(AmdPreset preset = null) - { - if (preset != null) - { - SetCurrentDisplay(preset); - } - - var display = _currentDisplay; - if (_currentDisplay.DisplayID.DisplayLogicalAdapterIndex == 0) - { - return []; - } - - var desktopRect = preset?.DisplayConfig.ApplyResolution == true && preset?.DisplayConfig.Resolution.ActiveWidth > 0 ? - new Rectangle(0, 0, (int)preset.DisplayConfig.Resolution.ActiveWidth, (int)preset.DisplayConfig.Resolution.ActiveHeight) : - GetDesktopRect(display); - - return GetAvailableRefreshRatesV2(Screen.PrimaryScreen.DeviceName, desktopRect.Width, desktopRect.Height); - } - - private Rectangle GetDesktopRect(ADLDisplayInfo display) - { - // TODO: improve this - var screen = Screen.AllScreens.First(); - - return screen.Bounds; - } - - public bool IsHDREnabled(ADLDisplayInfo display = default) - { - if (display.Equals(default(ADLDisplayInfo))) - { - display = GetCurrentDisplay(); - } - - var supported = false; - var enabled = false; - ADLWrapper.GetDisplayHDRState(display, ref supported, ref enabled); - - Logger.Debug($"IsHDREnabled: supported {supported}, enabled {enabled}"); - - return enabled; - } - - public List GetDisplays() - { - var displays = ADLWrapper.GetAllDisplays(); - - //if (!displays.Any()) - //{ - // displays.Add(new ADLDisplayInfo - // { - // DisplayName = "Dummy" - // }); - //} - - return displays; - } - - public string GetFullDisplayName(ADLDisplayInfo display) - { - var name = display.DisplayName; - - var screen = GetScreenForDisplay(display); - if (screen != null) - { - name = $"{screen.DeviceName} ({name})"; - } - - return name; - } - - public Screen GetScreenForDisplay(ADLDisplayInfo display) - { - var index = 0; - foreach (var screen in Screen.AllScreens) - { - var info = CCD.GetDisplayInfo(screen.DeviceName); - Logger.Debug($"SCREEN[{index}]: {screen.DeviceName}, {info?.FriendlyName}, {screen.Primary}"); - index++; - } - return Screen.AllScreens.FirstOrDefault(x => CCD.GetDisplayInfo(x.DeviceName)?.FriendlyName.Equals(display.DisplayName) ?? false); - } - - public List GetSimpleDisplayInfos() - { - List displays; - try - { - displays = GetDisplays(); - } - catch (Exception e) - { - Logger.Error("Error while getting displays: " + e.ToLogString()); - return null; - } - - return displays.Select(d => new AmdDisplayInfo(d, null, null)).ToList(); - } - - public List GetDisplayInfos() - { - var list = new List(); - - var displayInfos = GetSimpleDisplayInfos(); - - foreach (var displayInfo in displayInfos) - { - var preset = GetPresetForDisplay(displayInfo, displayInfos.Count()); - - displayInfo.Values = preset.GetDisplayValues(_globalContext.Config); - displayInfo.InfoLine = preset.InfoLine; - - list.Add(displayInfo); - } - - return list; - } - - protected override void Initialize() - { - if (!ADLWrapper.Initialize()) - { - throw new InvalidOperationException("Cannot load AMD API"); - } - } - - protected override void Uninitialize() - { - ADLWrapper.Uninitialze(); - } - - public List GetDisplayPresets() - { - try - { - var list = new List(); - - var displayInfos = GetSimpleDisplayInfos(); - - foreach (var displayInfo in displayInfos) - { - var preset = GetPresetForDisplay(displayInfo, displayInfos.Count()); - - displayInfo.Values = preset.GetDisplayValues(_globalContext.Config); - displayInfo.InfoLine = preset.InfoLine; - - list.Add(preset); - } - - return list; - } - catch (Exception e) - { - Logger.Error("Error while getting displays: " + e.ToLogString()); - return null; - } - } - - public AmdPreset GetPresetForDisplay(AmdDisplayInfo displayInfo, int displayCount = 1) - { - if (displayInfo == null) - { - return null; - } - - var display = displayInfo.Display; - - var colorDepth = ADLColorDepth.UNKNOWN; - ADLWrapper.GetDisplayColorDepth(display, ref colorDepth); - - var pixelFormat = ADLPixelFormat.UNKNOWN; - ADLWrapper.GetDisplayPixelFormat(display, ref pixelFormat); - - var ditherState = GetDithering(display); - - var screen = GetScreenForDisplay(display); - var displayConfig = CCD.GetDisplayConfig(screen?.DeviceName); - - var ccdDisplay = screen != null ? CCD.GetDisplayInfo(screen.DeviceName) : null; - - var hdrEnabled = IsHDREnabled(display); - - var preset = new AmdPreset - { - IsDisplayPreset = true, - displayName = display.DisplayName, - DisplayId = ccdDisplay?.DisplayId, - primaryDisplay = screen?.Primary == true, - colorDepth = colorDepth, - pixelFormat = pixelFormat, - ditherState = ditherState, - DisplayConfig = displayConfig, - SDRBrightness = hdrEnabled ? GetSDRBrightness(screen?.DeviceName) : null, - HDREnabled = hdrEnabled, - }; + class AmdService : GraphicsService + { + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + public override string ServiceName => "AMD"; + + protected override string PresetsBaseFilename => "AmdPresets.json"; + + private ADLDisplayInfo _currentDisplay; + + public static readonly int SHORTCUTID_AMDQA = -201; + + private string _configFilename; + public AmdServiceConfig Config { get; private set; } + + public AmdService(GlobalContext globalContext) : base(globalContext) + { + LoadPresets(); + LoadConfig(); + } + + protected override void AfterPresetsLoaded() + { + // Force disable display preset on presets + _presets.ForEach(p => p.IsDisplayPreset = false); + } + + public static async Task ExecutePresetAsync(string idOrName) + { + try + { + var globalContext = Program.ServiceProvider.GetRequiredService(); + + var service = new AmdService(globalContext); + + var result = await service.ApplyPreset(idOrName); + + if (!result) + { + Console.WriteLine("Preset not found or error while executing."); + } + + return result; + } + catch (Exception ex) + { + Console.WriteLine("Error executing preset: " + ex.ToLogString()); + return false; + } + } + + public override void InstallEventHandlers() + { + base.InstallEventHandlers(); + + SetShortcuts(SHORTCUTID_AMDQA, Config.QuickAccessShortcut); + } + + private void LoadConfig() + { + _configFilename = Path.Combine(_dataPath, "AmdConfig.json"); + try + { + if (File.Exists(_configFilename)) + { + Config = JsonConvert.DeserializeObject(File.ReadAllText(_configFilename)); + } + } + catch (Exception ex) + { + Logger.Error($"LoadConfig: {ex.Message}"); + } + Config ??= new AmdServiceConfig(); + } + + protected override List GetDefaultPresets() + { + return AmdPreset.GetDefaultPresets(); + } + + public override List GetInfo() + { + return [$"{_presets.Count} presets"]; + } + + public void GlobalSave() + { + SavePresets(); + SaveConfig(); + } + + public override bool HasDisplaysAttached(bool reinitialize = false) + { + try + { + return GetDisplays().Any(); + } + catch (Exception) + { + return false; + } + } + + public void SetCurrentDisplay(AmdPreset preset) + { + var displays = GetDisplays(); + if (preset.primaryDisplay) + { + _currentDisplay = displays.FirstOrDefault(); + } + else + { + _currentDisplay = displays.FirstOrDefault(x => x.DisplayName.Equals(preset.displayName)); + } + } + + public ADLDisplayInfo GetCurrentDisplay() + { + if (string.IsNullOrEmpty(_currentDisplay.DisplayName)) + { + _currentDisplay = GetDisplays().FirstOrDefault(); + } + + return _currentDisplay; + } + + public override Task ApplyPreset(AmdPreset preset) + { + SetCurrentDisplay(preset); + + if (string.IsNullOrEmpty(_currentDisplay.DisplayName)) + { + Logger.Error("Could not set current display"); + return Task.FromResult(false); + } + + var result = true; + + if (preset.applyHDR) + { + var hdrEnabled = IsHDREnabled(); + if (preset.toggleHDR) + { + result = ADLWrapper.SetDisplayHDRState(_currentDisplay, !hdrEnabled); + } + else + { + result = ADLWrapper.SetDisplayHDRState(_currentDisplay, preset.HDREnabled); + } + + //if (preset.toggleHDR || preset.HDREnabled != hdrEnabled) + //{ + // ToggleHDR(config.DisplaySettingsDelay); + //} + + if (preset.SDRBrightness.HasValue) + { + var deviceName = GetDisplayDeviceName(_currentDisplay); + + SetSDRBrightness(deviceName, preset.SDRBrightness.Value); + } + } + + if (preset.DisplayConfig.ApplyRefreshRate || preset.DisplayConfig.ApplyResolution) + { + if (!SetMode(preset.DisplayConfig, true)) + { + result = false; + } + } + + if (preset.applyColorData) + { + if (!ADLWrapper.SetDisplayColorDepth(_currentDisplay, preset.colorDepth)) + { + Logger.Error("Could not set color depth"); + result = false; + } + if (preset.pixelFormat != ADLPixelFormat.UNKNOWN && !ADLWrapper.SetDisplayPixelFormat(_currentDisplay, preset.pixelFormat)) + { + Logger.Error("Could not set pixel format"); + result = false; + } + } + + if (preset.applyDithering) + { + if (!SetDithering(preset.ditherState)) + { + result = false; + } + } + + _lastAppliedPreset = preset; + + PresetApplied(); + + return Task.FromResult(result); + } + + public bool SetDithering(ADLDitherState ditherState) + { + var display = GetCurrentDisplay(); + + return ADLWrapper.SetDisplayDitherState(display, ditherState); + } + + public ADLDitherState GetDithering(ADLDisplayInfo display = default(ADLDisplayInfo)) + { + if (string.IsNullOrEmpty(display.DisplayName)) + { + display = GetCurrentDisplay(); + } + + var ditherState = ADLDitherState.DRIVER_DEFAULT; + + if (ADLWrapper.GetDisplayDitherState(display, ref ditherState)) + { + return ditherState; + } + + return ditherState; + } + + public string GetDisplayDeviceName(ADLDisplayInfo display) + { + return Screen.PrimaryScreen.DeviceName; + } + + public bool SetMode(DisplayConfig displayConfig, bool updateRegistry = false) + { + var display = GetCurrentDisplay(); + + return SetMode(GetDisplayDeviceName(display), displayConfig, updateRegistry); + } + + public List GetAvailableRefreshRates(AmdPreset preset = null) + { + if (preset != null) + { + SetCurrentDisplay(preset); + } + + var horizontal = 3840; + var vertical = 2160; + var display = GetCurrentDisplay(); + + ADLWrapper.GetDisplayResolution(display, ref horizontal, ref vertical); + + return GetAvailableRefreshRatesV2(GetDisplayDeviceName(display), horizontal, vertical); + } + + public List GetAvailableResolutionsV2(AmdPreset preset = null) + { + if (preset != null) + { + SetCurrentDisplay(preset); + } + + var display = GetCurrentDisplay(); + var deviceName = GetDisplayDeviceName(display); + if (deviceName == null) + { + return []; + } + + return GetAvailableResolutionsInternalV2(deviceName); + } + + public bool SetMode(VirtualResolution resolution = null, Rational refreshRate = null, bool updateRegistry = false, AmdPreset preset = null) + { + var display = preset == null ? GetCurrentDisplay() : GetPresetDisplay(preset); + if (display.Equals(default(ADLDisplayInfo))) + { + return false; + } + + return SetMode(GetDisplayDeviceName(display), resolution, refreshRate, updateRegistry); + } + + public bool SetMode(DisplayConfig displayConfig, bool updateRegistry = false, AmdPreset preset = null) + { + var display = preset == null ? GetCurrentDisplay() : GetPresetDisplay(preset); + if (display.DisplayID.DisplayPhysicalIndex == 0) + { + return false; + } + + return SetMode(GetDisplayDeviceName(display), displayConfig, updateRegistry); + } + + public ADLDisplayInfo GetPresetDisplay(AmdPreset preset) + { + return GetCurrentDisplay(); + } + + public List GetAvailableRefreshRatesV2(AmdPreset preset = null) + { + if (preset != null) + { + SetCurrentDisplay(preset); + } + + var display = _currentDisplay; + if (_currentDisplay.DisplayID.DisplayLogicalAdapterIndex == 0) + { + return []; + } + + var desktopRect = preset?.DisplayConfig.ApplyResolution == true && preset?.DisplayConfig.Resolution.ActiveWidth > 0 ? + new Rectangle(0, 0, (int)preset.DisplayConfig.Resolution.ActiveWidth, (int)preset.DisplayConfig.Resolution.ActiveHeight) : + GetDesktopRect(display); + + return GetAvailableRefreshRatesV2(Screen.PrimaryScreen.DeviceName, desktopRect.Width, desktopRect.Height); + } + + private Rectangle GetDesktopRect(ADLDisplayInfo display) + { + // TODO: improve this + var screen = Screen.AllScreens.First(); + + return screen.Bounds; + } + + public bool IsHDREnabled(ADLDisplayInfo display = default) + { + if (display.Equals(default(ADLDisplayInfo))) + { + display = GetCurrentDisplay(); + } + + var supported = false; + var enabled = false; + ADLWrapper.GetDisplayHDRState(display, ref supported, ref enabled); + + Logger.Debug($"IsHDREnabled: supported {supported}, enabled {enabled}"); + + return enabled; + } + + public List GetDisplays() + { + var displays = ADLWrapper.GetAllDisplays(); + + //if (!displays.Any()) + //{ + // displays.Add(new ADLDisplayInfo + // { + // DisplayName = "Dummy" + // }); + //} + + return displays; + } + + public string GetFullDisplayName(ADLDisplayInfo display) + { + var name = display.DisplayName; + + var screen = GetScreenForDisplay(display); + if (screen != null) + { + name = $"{screen.DeviceName} ({name})"; + } + + return name; + } + + public Screen GetScreenForDisplay(ADLDisplayInfo display) + { + var index = 0; + foreach (var screen in Screen.AllScreens) + { + var info = CCD.GetDisplayInfo(screen.DeviceName); + Logger.Debug($"SCREEN[{index}]: {screen.DeviceName}, {info?.FriendlyName}, {screen.Primary}"); + index++; + } + return Screen.AllScreens.FirstOrDefault(x => CCD.GetDisplayInfo(x.DeviceName)?.FriendlyName.Equals(display.DisplayName) ?? false); + } + + public List GetSimpleDisplayInfos() + { + List displays; + try + { + displays = GetDisplays(); + } + catch (Exception e) + { + Logger.Error("Error while getting displays: " + e.ToLogString()); + return null; + } + + return displays.Select(d => new AmdDisplayInfo(d, null, null)).ToList(); + } + + public List GetDisplayInfos() + { + var list = new List(); + + var displayInfos = GetSimpleDisplayInfos(); + + foreach (var displayInfo in displayInfos) + { + var preset = GetPresetForDisplay(displayInfo, displayInfos.Count()); + + displayInfo.Values = preset.GetDisplayValues(_globalContext.Config); + displayInfo.InfoLine = preset.InfoLine; + + list.Add(displayInfo); + } + + return list; + } + + protected override void Initialize() + { + if (!ADLWrapper.Initialize()) + { + throw new InvalidOperationException("Cannot load AMD API"); + } + } + + protected override void Uninitialize() + { + ADLWrapper.Uninitialze(); + } + + public List GetDisplayPresets() + { + try + { + var list = new List(); + + var displayInfos = GetSimpleDisplayInfos(); + + foreach (var displayInfo in displayInfos) + { + var preset = GetPresetForDisplay(displayInfo, displayInfos.Count()); + + displayInfo.Values = preset.GetDisplayValues(_globalContext.Config); + displayInfo.InfoLine = preset.InfoLine; + + list.Add(preset); + } + + return list; + } + catch (Exception e) + { + Logger.Error("Error while getting displays: " + e.ToLogString()); + return null; + } + } + + public AmdPreset GetPresetForDisplay(AmdDisplayInfo displayInfo, int displayCount = 1) + { + if (displayInfo == null) + { + return null; + } + + var display = displayInfo.Display; + + var colorDepth = ADLColorDepth.UNKNOWN; + ADLWrapper.GetDisplayColorDepth(display, ref colorDepth); + + var pixelFormat = ADLPixelFormat.UNKNOWN; + ADLWrapper.GetDisplayPixelFormat(display, ref pixelFormat); + + var ditherState = GetDithering(display); + + var screen = GetScreenForDisplay(display); + var displayConfig = CCD.GetDisplayConfig(screen?.DeviceName); + + var ccdDisplay = screen != null ? CCD.GetDisplayInfo(screen.DeviceName) : null; + + var hdrEnabled = IsHDREnabled(display); - return preset; - } + var preset = new AmdPreset + { + IsDisplayPreset = true, + displayName = display.DisplayName, + DisplayId = ccdDisplay?.DisplayId, + primaryDisplay = screen?.Primary == true, + colorDepth = colorDepth, + pixelFormat = pixelFormat, + ditherState = ditherState, + DisplayConfig = displayConfig, + SDRBrightness = hdrEnabled ? GetSDRBrightness(screen?.DeviceName) : null, + HDREnabled = hdrEnabled, + }; - private void SaveConfig() - { - Utils.WriteObject(_configFilename, Config); - } + return preset; + } - public AmdServiceConfig GetConfig() - { - return Config; - } + private void SaveConfig() + { + Utils.WriteObject(_configFilename, Config); + } - public bool UpdateConfig(AmdServiceConfig config) - { - Config.Update(config); + public AmdServiceConfig GetConfig() + { + return Config; + } - SetShortcuts(SHORTCUTID_AMDQA, Config.QuickAccessShortcut); + public bool UpdateConfig(AmdServiceConfig config) + { + Config.Update(config); - SaveConfig(); + SetShortcuts(SHORTCUTID_AMDQA, Config.QuickAccessShortcut); - return true; - } + SaveConfig(); - public bool UpdatePreset(AmdPreset specPreset) - { - var currentPreset = _presets.FirstOrDefault(p => p.id == specPreset.id); + return true; + } - if (currentPreset != null) - { - currentPreset.name = specPreset.name; - currentPreset.shortcut = specPreset.shortcut; - currentPreset.Update(specPreset); + public bool UpdatePreset(AmdPreset specPreset) + { + ValidatePreset(specPreset); - SavePresets(); + var currentPreset = _presets.FirstOrDefault(p => p.id == specPreset.id); - return true; - } + if (currentPreset != null) + { + currentPreset.name = specPreset.name; + currentPreset.shortcut = specPreset.shortcut; + currentPreset.Update(specPreset); - var newPreset = new AmdPreset(specPreset); - newPreset.name = specPreset.name; - newPreset.shortcut = specPreset.shortcut; + SavePresets(currentPreset); - _presets.Add(newPreset); + return true; + } - SavePresets(); + var newPreset = new AmdPreset(specPreset); + newPreset.name = specPreset.name; + newPreset.shortcut = specPreset.shortcut; - return true; - } - } + _presets.Add(newPreset); + + SavePresets(newPreset); + + return true; + } + } } diff --git a/ColorControl/Services/Common/ServiceBase.cs b/ColorControl/Services/Common/ServiceBase.cs index da339d6..e063a4d 100644 --- a/ColorControl/Services/Common/ServiceBase.cs +++ b/ColorControl/Services/Common/ServiceBase.cs @@ -38,6 +38,7 @@ namespace ColorControl.Services.Common protected int _quickAccessShortcutId; protected GlobalContext _globalContext; + private KeyboardShortcutDispatcher _keyboardShortcutDispatcher; private List _jsonConverters; @@ -147,28 +148,33 @@ namespace ColorControl.Services.Common return await ApplyPresetUi(preset); } + private void LoadServices() + { + _keyboardShortcutDispatcher ??= _globalContext.ServiceProvider.GetService(); + } + protected void SetShortcuts(int quickAccessShortcutId = 0, string shortcut = null) { _quickAccessShortcutId = quickAccessShortcutId; - var keyboardShortcutDispatcher = _globalContext.ServiceProvider.GetService(); + LoadServices(); - if (keyboardShortcutDispatcher == null) + if (_keyboardShortcutDispatcher == null) { return; } if (quickAccessShortcutId != 0) { - keyboardShortcutDispatcher.RegisterShortcut(quickAccessShortcutId, shortcut); + _keyboardShortcutDispatcher.RegisterShortcut(quickAccessShortcutId, shortcut); } foreach (var preset in _presets) { - keyboardShortcutDispatcher.RegisterShortcut(preset.id, preset.shortcut); + _keyboardShortcutDispatcher.RegisterShortcut(preset.id, preset.shortcut); } - keyboardShortcutDispatcher.RegisterAsyncEventHandler(KeyboardShortcutDispatcher.Event_HotKey, HotKeyPressed); + _keyboardShortcutDispatcher.RegisterAsyncEventHandler(KeyboardShortcutDispatcher.Event_HotKey, HotKeyPressed); } public virtual void InstallEventHandlers() @@ -288,8 +294,23 @@ namespace ColorControl.Services.Common } - protected void SavePresets() + protected void ValidatePreset(PresetBase preset) { + if (!KeyboardShortcutDispatcher.ValidateShortcut(preset.shortcut, false)) + { + throw new InvalidOperationException("Invalid shortcut"); + } + } + + protected void SavePresets(PresetBase updatedPreset = null) + { + if (updatedPreset != null) + { + LoadServices(); + + _keyboardShortcutDispatcher?.RegisterShortcut(updatedPreset.id, updatedPreset.shortcut); + } + Utils.WriteObject(_presetsFilename, _presets); } diff --git a/ColorControl/Services/GameLauncher/GameService.cs b/ColorControl/Services/GameLauncher/GameService.cs index 4079c5b..769ce8f 100644 --- a/ColorControl/Services/GameLauncher/GameService.cs +++ b/ColorControl/Services/GameLauncher/GameService.cs @@ -15,370 +15,372 @@ using System.Threading.Tasks; namespace ColorControl.Services.GameLauncher { - class GameService : ServiceBase - { - private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - - public override string ServiceName => "Game"; - - protected override string PresetsBaseFilename => "GamePresets.json"; - - private ProcessEventDispatcher _processEventDispatcher; - private readonly WinApiAdminService _winApiAdminService; - private readonly WinApiService _winApiService; - private ServiceManager _serviceManager; - private int _lastStartedProcessId; - private System.DateTime _lastStartedPresetDateTime = System.DateTime.MinValue; - private string _lastStartedProcessPath; - private string _configFilename; - - public GameServiceConfig Config { get; private set; } - - public static readonly int SHORTCUTID_GAMEQA = -203; - - public GameService(GlobalContext globalContext, ServiceManager serviceManager, ProcessEventDispatcher processEventDispatcher, WinApiAdminService winApiAdminService, WinApiService winApiService) : base(globalContext) - { - LoadConfig(); - LoadPresets(); - _serviceManager = serviceManager; - _processEventDispatcher = processEventDispatcher; - _winApiAdminService = winApiAdminService; - _winApiService = winApiService; - _processEventDispatcher.RegisterAsyncEventHandler(ProcessEventDispatcher.Event_ProcessChanged, ProcessChanged); - } - - public override void InstallEventHandlers() - { - base.InstallEventHandlers(); - - SetShortcuts(SHORTCUTID_GAMEQA, Config.QuickAccessShortcut); - } - - public override List GetInfo() - { - return [$"{_presets.Count} presets"]; - } - - private async Task ProcessChanged(object sender, ProcessChangedEventArgs e, CancellationToken token) - { - if (e.StartedProcesses?.Any() != true || !Config.ApplyExternallyLaunched) - { - return; - } - - if (_lastStartedPresetDateTime > System.DateTime.Now.AddSeconds(-5)) - { - return; - } - _lastStartedProcessId = 0; - - var startedProcesses = e.StartedProcesses; - - foreach (var startedProcess in startedProcesses) - { - try - { - if (await HandleStartedProcess(startedProcess)) - { - return; - } - } - catch (System.Exception) { } - } - } - - private Task HandleStartedProcess(Process process) - { - GamePreset preset; - - if (_winApiService.IsAdministrator()) - { - var mainModule = process.MainModule; - - if (mainModule == null) - { - return Task.FromResult(false); - } - - var fileName = mainModule.FileName; - - preset = _presets.FirstOrDefault(p => fileName.Contains(Path.GetDirectoryName(p.Path), System.StringComparison.OrdinalIgnoreCase)); - } - else - { - var name = process.ProcessName.Replace("-Win64-Shipping", ""); - preset = _presets.FirstOrDefault(p => Path.GetFileName(p.Path).Contains(name, System.StringComparison.OrdinalIgnoreCase)); - } - - if (preset == null || !preset.AutoSettings.AllowAutoApply) - { - return Task.FromResult(false); - } - - if (_lastStartedProcessId != process.Id) - { - Logger.Debug($"Auto-starting preset {preset.name} for process {process.ProcessName}"); - - var mainThreadId = 0; - var suspended = false; - if (preset.AutoSettings.ProcessAutoAction == ProcessAutoAction.Suspend) - { - mainThreadId = _winApiAdminService.SuspendMainThread(process.Id); - suspended = true; - } - else if (preset.AutoSettings.ProcessAutoAction == ProcessAutoAction.Restart) - { - _winApiAdminService.StopProcess(process.Id); - process = null; - } - - var _ = ExecutePreset(preset, process, suspended ? mainThreadId : 0); - - return Task.FromResult(true); - } - - return Task.FromResult(false); - } - - protected override List GetDefaultPresets() - { - return new List(); - } - - public void GlobalSave() - { - SavePresets(); - SaveConfig(); - } - - public override async Task ApplyPreset(GamePreset preset) - { - var result = await ExecutePreset(preset); - - _lastAppliedPreset = preset; - - PresetApplied(); - - return result; - } - - private async Task ExecutePreset(GamePreset preset, Process process = null, int resumeThreadId = default) - { - var result = true; - - await ExecuteSteps(preset.PreLaunchSteps); - - if (process == null && !string.IsNullOrEmpty(preset.Path)) - { - process = _winApiAdminService.StartProcess(preset.Path, preset.Parameters, setWorkingDir: true, elevate: preset.RunAsAdministrator, affinityMask: preset.ProcessAffinityMask, priorityClass: preset.ProcessPriorityClass); - - _lastStartedPresetDateTime = System.DateTime.Now; - _lastStartedProcessId = process.Id; - _lastStartedProcessPath = Path.GetDirectoryName(preset.Path); - } - else if (resumeThreadId != default) - { - _winApiAdminService.ResumeThread((uint)resumeThreadId); - } - - await ExecuteSteps(preset.PostLaunchSteps); - - if (process != null && preset.FinalizeSteps?.Any() == true) - { - await ExecuteFinalizationStepsAsync(process, preset.FinalizeSteps); - } - - return result; - } - - private async Task ExecuteFinalizationStepsAsync(Process process, List finalizeSteps) - { - await Task.Delay(1000); - - try - { - if (process.HasExited) - { - return; - } - - await process.WaitForExitAsync(); - } - catch (Exception ex) - { - Logger.Error(ex); - return; - } - - await ExecuteSteps(finalizeSteps); - } - - private async Task ExecuteSteps(List steps) - { - if (steps?.Any() != true) - { - return; - } - - foreach (var step in steps) - { - var keySpec = step.Split(':'); - - var delay = 0; - var key = step; - if (keySpec.Length == 2) - { - delay = Utils.ParseInt(keySpec[1]); - if (delay > 0) - { - key = keySpec[0]; - } - } - - var index = key.IndexOf("("); - string[] parameters = null; - if (index > -1) - { - var keyValue = key.Split('('); - key = keyValue[0]; - parameters = keyValue[1].Substring(0, keyValue[1].Length - 1).Trim().Split(';'); - } - - var handled = false; - - if (parameters != null) - { - handled = await _serviceManager.HandleExternalServiceAsync(key, parameters); - } - - if (!handled) - { - } - - if (delay > 0) - { - await Task.Delay(delay); - } - - } - } - - private void LoadConfig() - { - _configFilename = Path.Combine(_dataPath, "GameConfig.json"); - try - { - if (File.Exists(_configFilename)) - { - Config = JsonConvert.DeserializeObject(File.ReadAllText(_configFilename)); - } - } - catch (System.Exception ex) - { - Logger.Error($"LoadConfig: {ex.Message}"); - } - Config ??= new GameServiceConfig(); - } - - private void SaveConfig() - { - Utils.WriteObject(_configFilename, Config); - } - - public GameServiceConfig GetConfig() - { - return Config; - } - - public bool UpdateConfig(GameServiceConfig config) - { - Config.Update(config); - - SetShortcuts(SHORTCUTID_GAMEQA, Config.QuickAccessShortcut); - - SaveConfig(); - - return true; - } - - public bool UpdatePreset(GamePreset presetSpec) - { - var preset = _presets.FirstOrDefault(x => x.id == presetSpec.id); - - if (preset != null) - { - preset.Update(presetSpec); - } - else - { - preset = new GamePreset(presetSpec); - _presets.Add(preset); - } - - SavePresets(); - - return true; - } - - public string SelectPath() - { - var fileInfo = Utils.SelectFile(); - - if (fileInfo == null) - { - return null; - } - - return fileInfo.FullName; - } - - public List GetMruApps(bool skipExistingPresets = false) - { - var key = Registry.ClassesRoot.OpenSubKey(@"Local Settings\Software\Microsoft\Windows\Shell\MuiCache"); - - if (key == null) - { - return new List(); - } - - var names = key.GetValueNames(); - var apps = new List(); - var winFolder = Environment.GetFolderPath(Environment.SpecialFolder.Windows); - - foreach (var name in names) - { - var parts = name.Split('.'); - - if (parts.Last() != "FriendlyAppName") - { - continue; - } - - var path = name.Replace("." + parts.Last(), ""); - - if (skipExistingPresets && _presets.Any(p => p.Path.Equals(path, StringComparison.OrdinalIgnoreCase))) - { - continue; - } - - if (!path.StartsWith(winFolder) && Path.Exists(path)) - { - var friendlyName = key.GetValue(name)?.ToString(); - - var app = new GameApp - { - Path = path, - Filename = Path.GetFileName(path), - FriendlyName = friendlyName - }; - - apps.Add(app); - } - } - - apps = apps.DistinctBy(a => a.Path).OrderBy(a => a.FriendlyName).ToList(); - - return apps; - } - } + class GameService : ServiceBase + { + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + public override string ServiceName => "Game"; + + protected override string PresetsBaseFilename => "GamePresets.json"; + + private ProcessEventDispatcher _processEventDispatcher; + private readonly WinApiAdminService _winApiAdminService; + private readonly WinApiService _winApiService; + private ServiceManager _serviceManager; + private int _lastStartedProcessId; + private System.DateTime _lastStartedPresetDateTime = System.DateTime.MinValue; + private string _lastStartedProcessPath; + private string _configFilename; + + public GameServiceConfig Config { get; private set; } + + public static readonly int SHORTCUTID_GAMEQA = -203; + + public GameService(GlobalContext globalContext, ServiceManager serviceManager, ProcessEventDispatcher processEventDispatcher, WinApiAdminService winApiAdminService, WinApiService winApiService) : base(globalContext) + { + LoadConfig(); + LoadPresets(); + _serviceManager = serviceManager; + _processEventDispatcher = processEventDispatcher; + _winApiAdminService = winApiAdminService; + _winApiService = winApiService; + _processEventDispatcher.RegisterAsyncEventHandler(ProcessEventDispatcher.Event_ProcessChanged, ProcessChanged); + } + + public override void InstallEventHandlers() + { + base.InstallEventHandlers(); + + SetShortcuts(SHORTCUTID_GAMEQA, Config.QuickAccessShortcut); + } + + public override List GetInfo() + { + return [$"{_presets.Count} presets"]; + } + + private async Task ProcessChanged(object sender, ProcessChangedEventArgs e, CancellationToken token) + { + if (e.StartedProcesses?.Any() != true || !Config.ApplyExternallyLaunched) + { + return; + } + + if (_lastStartedPresetDateTime > System.DateTime.Now.AddSeconds(-5)) + { + return; + } + _lastStartedProcessId = 0; + + var startedProcesses = e.StartedProcesses; + + foreach (var startedProcess in startedProcesses) + { + try + { + if (await HandleStartedProcess(startedProcess)) + { + return; + } + } + catch (System.Exception) { } + } + } + + private Task HandleStartedProcess(Process process) + { + GamePreset preset; + + if (_winApiService.IsAdministrator()) + { + var mainModule = process.MainModule; + + if (mainModule == null) + { + return Task.FromResult(false); + } + + var fileName = mainModule.FileName; + + preset = _presets.FirstOrDefault(p => fileName.Contains(Path.GetDirectoryName(p.Path), System.StringComparison.OrdinalIgnoreCase)); + } + else + { + var name = process.ProcessName.Replace("-Win64-Shipping", ""); + preset = _presets.FirstOrDefault(p => Path.GetFileName(p.Path).Contains(name, System.StringComparison.OrdinalIgnoreCase)); + } + + if (preset == null || !preset.AutoSettings.AllowAutoApply) + { + return Task.FromResult(false); + } + + if (_lastStartedProcessId != process.Id) + { + Logger.Debug($"Auto-starting preset {preset.name} for process {process.ProcessName}"); + + var mainThreadId = 0; + var suspended = false; + if (preset.AutoSettings.ProcessAutoAction == ProcessAutoAction.Suspend) + { + mainThreadId = _winApiAdminService.SuspendMainThread(process.Id); + suspended = true; + } + else if (preset.AutoSettings.ProcessAutoAction == ProcessAutoAction.Restart) + { + _winApiAdminService.StopProcess(process.Id); + process = null; + } + + var _ = ExecutePreset(preset, process, suspended ? mainThreadId : 0); + + return Task.FromResult(true); + } + + return Task.FromResult(false); + } + + protected override List GetDefaultPresets() + { + return new List(); + } + + public void GlobalSave() + { + SavePresets(); + SaveConfig(); + } + + public override async Task ApplyPreset(GamePreset preset) + { + var result = await ExecutePreset(preset); + + _lastAppliedPreset = preset; + + PresetApplied(); + + return result; + } + + private async Task ExecutePreset(GamePreset preset, Process process = null, int resumeThreadId = default) + { + var result = true; + + await ExecuteSteps(preset.PreLaunchSteps); + + if (process == null && !string.IsNullOrEmpty(preset.Path)) + { + process = _winApiAdminService.StartProcess(preset.Path, preset.Parameters, setWorkingDir: true, elevate: preset.RunAsAdministrator, affinityMask: preset.ProcessAffinityMask, priorityClass: preset.ProcessPriorityClass); + + _lastStartedPresetDateTime = System.DateTime.Now; + _lastStartedProcessId = process.Id; + _lastStartedProcessPath = Path.GetDirectoryName(preset.Path); + } + else if (resumeThreadId != default) + { + _winApiAdminService.ResumeThread((uint)resumeThreadId); + } + + await ExecuteSteps(preset.PostLaunchSteps); + + if (process != null && preset.FinalizeSteps?.Any() == true) + { + await ExecuteFinalizationStepsAsync(process, preset.FinalizeSteps); + } + + return result; + } + + private async Task ExecuteFinalizationStepsAsync(Process process, List finalizeSteps) + { + await Task.Delay(1000); + + try + { + if (process.HasExited) + { + return; + } + + await process.WaitForExitAsync(); + } + catch (Exception ex) + { + Logger.Error(ex); + return; + } + + await ExecuteSteps(finalizeSteps); + } + + private async Task ExecuteSteps(List steps) + { + if (steps?.Any() != true) + { + return; + } + + foreach (var step in steps) + { + var keySpec = step.Split(':'); + + var delay = 0; + var key = step; + if (keySpec.Length == 2) + { + delay = Utils.ParseInt(keySpec[1]); + if (delay > 0) + { + key = keySpec[0]; + } + } + + var index = key.IndexOf("("); + string[] parameters = null; + if (index > -1) + { + var keyValue = key.Split('('); + key = keyValue[0]; + parameters = keyValue[1].Substring(0, keyValue[1].Length - 1).Trim().Split(';'); + } + + var handled = false; + + if (parameters != null) + { + handled = await _serviceManager.HandleExternalServiceAsync(key, parameters); + } + + if (!handled) + { + } + + if (delay > 0) + { + await Task.Delay(delay); + } + + } + } + + private void LoadConfig() + { + _configFilename = Path.Combine(_dataPath, "GameConfig.json"); + try + { + if (File.Exists(_configFilename)) + { + Config = JsonConvert.DeserializeObject(File.ReadAllText(_configFilename)); + } + } + catch (System.Exception ex) + { + Logger.Error($"LoadConfig: {ex.Message}"); + } + Config ??= new GameServiceConfig(); + } + + private void SaveConfig() + { + Utils.WriteObject(_configFilename, Config); + } + + public GameServiceConfig GetConfig() + { + return Config; + } + + public bool UpdateConfig(GameServiceConfig config) + { + Config.Update(config); + + SetShortcuts(SHORTCUTID_GAMEQA, Config.QuickAccessShortcut); + + SaveConfig(); + + return true; + } + + public bool UpdatePreset(GamePreset specPreset) + { + ValidatePreset(specPreset); + + var preset = _presets.FirstOrDefault(x => x.id == specPreset.id); + + if (preset != null) + { + preset.Update(specPreset); + } + else + { + preset = new GamePreset(specPreset); + _presets.Add(preset); + } + + SavePresets(preset); + + return true; + } + + public string SelectPath() + { + var fileInfo = Utils.SelectFile(); + + if (fileInfo == null) + { + return null; + } + + return fileInfo.FullName; + } + + public List GetMruApps(bool skipExistingPresets = false) + { + var key = Registry.ClassesRoot.OpenSubKey(@"Local Settings\Software\Microsoft\Windows\Shell\MuiCache"); + + if (key == null) + { + return new List(); + } + + var names = key.GetValueNames(); + var apps = new List(); + var winFolder = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + + foreach (var name in names) + { + var parts = name.Split('.'); + + if (parts.Last() != "FriendlyAppName") + { + continue; + } + + var path = name.Replace("." + parts.Last(), ""); + + if (skipExistingPresets && _presets.Any(p => p.Path.Equals(path, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + if (!path.StartsWith(winFolder) && Path.Exists(path)) + { + var friendlyName = key.GetValue(name)?.ToString(); + + var app = new GameApp + { + Path = path, + Filename = Path.GetFileName(path), + FriendlyName = friendlyName + }; + + apps.Add(app); + } + } + + apps = apps.DistinctBy(a => a.Path).OrderBy(a => a.FriendlyName).ToList(); + + return apps; + } + } } diff --git a/ColorControl/Services/LG/LgDevice.cs b/ColorControl/Services/LG/LgDevice.cs index 0128e94..950db59 100644 --- a/ColorControl/Services/LG/LgDevice.cs +++ b/ColorControl/Services/LG/LgDevice.cs @@ -14,1354 +14,1354 @@ using System.Threading.Tasks; namespace ColorControl.Services.LG { - class LgDevice - { - public class InvokableAction - { - public Func, Task> AsyncFunction { get; set; } - public string Name { get; set; } - public Type EnumType { get; set; } - public decimal MinValue { get; set; } - public decimal MaxValue { get; set; } - public string Category { get; set; } - public string Title { get; set; } - public int CurrentValue { get; set; } - public int NumberOfValues { get; set; } - public LgPreset Preset { get; set; } - public bool Advanced { get; set; } - public ModelYear FromModelYear { get; set; } = ModelYear.None; - public ModelYear ToModelYear { get; set; } = ModelYear.None; - public List ValueLabels { get; set; } - } - - public class LgDevicePictureSettings - { - public int Backlight { get; set; } - public int Contrast { get; set; } - public int Brightness { get; set; } - public int Color { get; set; } - public int Volume { get; set; } - } - - public enum PowerState - { - Unknown, - Active, - Power_Off, - Suspend, - Active_Standby, - Screen_Off - } - - public enum PowerOffSource - { - Unknown, - App, - Manually - } - - public enum ModelYear - { - None = 0, - SeriesPre2018, - Series2018, - Series2019, - Series2020, - Series2021, - Series2022, - Series2023 - } - - protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - public static List DefaultActionsOnGameBar = new() { "backlight", "contrast", "brightness", "color" }; - - private static List IreValueLabels = ["IRE 2.5", "IRE 5", "IRE 7.5", "IRE 10", "IRE 15", "IRE 20", "IRE 25", "IRE 30", "IRE 35", "IRE 40", "IRE 45", "IRE 50", "IRE 55", "IRE 60", "IRE 65", "IRE 70", "IRE 75", "IRE 80", "IRE 85", "IRE 90", "IRE 95", "IRE 100"]; - private static List Ire10ptValueLabels = ["IRE 10", "IRE 20", "IRE 30", "IRE 40", "IRE 50", "IRE 60", "IRE 70", "IRE 80", "IRE 90", "IRE 100"]; - - public string Name { get; private set; } - public string IpAddress { get; private set; } - public string MacAddress { get; private set; } - public bool IsCustom { get; private set; } - [JsonIgnore] - public bool IsDummy { get; private set; } - - public bool PowerOnAfterStartup { get; set; } - public bool PowerOnAfterResume { get; set; } - public bool PowerOffOnShutdown { get; set; } - public bool PowerOffOnStandby { get; set; } - public bool PowerOffOnScreenSaver { get; set; } - public int ScreenSaverMinimalDuration { get; set; } - public bool TurnScreenOffOnScreenSaver { get; set; } - public bool HandleManualScreenSaver { get; set; } - public bool PowerOnAfterScreenSaver { get; set; } - public bool TurnScreenOnAfterScreenSaver { get; set; } - public bool PowerOnAfterManualPowerOff { get; set; } - public bool PowerOnByWindows { get; set; } - public bool PowerOffByWindows { get; set; } - public bool PowerOnAfterSessionUnlock { get; set; } - public bool PowerOffAfterSessionLock { get; set; } - public bool TriggersEnabled { get; set; } - public int HDMIPortNumber { get; set; } - - public bool UseSecureConnection { get; set; } = true; - - private List _actionsForGameBar; - - public List ActionsOnGameBar - { - get - { - return _actionsForGameBar; - } - set - { - if (value?.Any() == false) - { - _actionsForGameBar = DefaultActionsOnGameBar; - } - else - { - _actionsForGameBar = value; - } - } - } - - [JsonIgnore] - public PowerState CurrentState { get; set; } - - [JsonIgnore] - private LgTvApi _lgTvApi; - [JsonIgnore] - private bool _justWokeUp; - [JsonIgnore] - public PowerOffSource PoweredOffBy { get; private set; } - - [JsonIgnore] - public bool PoweredOffViaApp { get; private set; } - [JsonIgnore] - private DateTimeOffset _poweredOffViaAppDateTime { get; set; } - [JsonIgnore] - private List _invokableActions = new List(); - [JsonIgnore] - private SemaphoreSlim _connectSemaphore = new SemaphoreSlim(1, 1); - - [JsonIgnore] - public string ModelName { get; private set; } - [JsonIgnore] - public ModelYear Year { get; private set; } - - [JsonIgnore] - public LgDevicePictureSettings PictureSettings { get; private set; } - - [JsonIgnore] - public string CurrentAppId { get; private set; } - - public event EventHandler PictureSettingsChangedEvent; - public event EventHandler PowerStateChangedEvent; - - [JsonIgnore] - private Timer _powerOffTimer; - - [JsonIgnore] - private ServiceManager _serviceManager; - - [JsonConstructor] - public LgDevice(string name, string ipAddress, string macAddress, bool isCustom = true, bool isDummy = false) - { - PictureSettings = new LgDevicePictureSettings(); - ActionsOnGameBar = new List(DefaultActionsOnGameBar); - - Name = name; - IpAddress = ipAddress; - MacAddress = macAddress; - IsCustom = isCustom; - IsDummy = isDummy; - TriggersEnabled = true; - - AddInvokableAction("WOL", WakeAction); - AddInvokableAction("Reboot", RebootAction); - AddGenericPictureAction("backlight", minValue: 0, maxValue: 100); - AddGenericPictureAction("brightness", minValue: 0, maxValue: 100, title: "Brightness/Black Level"); - AddGenericPictureAction("contrast", minValue: 0, maxValue: 100); - AddGenericPictureAction("color", minValue: 0, maxValue: 100); - AddGenericPictureAction("pictureMode", typeof(PictureMode), title: "Picture Mode"); - AddGenericPictureAction("sharpness", minValue: 0, maxValue: 50); - //AddGenericPictureAction("dynamicRange", typeof(DynamicRange), category: "dimensionInfo", title: "Dynamic Range"); - AddGenericPictureAction("colorGamut", typeof(ColorGamut), title: "Color Gamut"); - AddGenericPictureAction("dynamicContrast", typeof(OffToHigh), title: "Dynamic Contrast"); - AddGenericPictureAction("superResolution", typeof(OffToHigh), title: "Super Resolution"); - AddGenericPictureAction("gamma", typeof(GammaExp)); - AddGenericPictureAction("colorTemperature", minValue: -50, maxValue: 50, title: "Color Temperature"); - AddGenericPictureAction("whiteBalanceColorTemperature", typeof(WhiteBalanceColorTemperature), title: "White Balance Color Temperature"); - AddGenericPictureAction("eyeComfortMode", typeof(OffToOn), title: "Eye Comfort Mode"); - //AddGenericPictureAction("dynamicColor", typeof(OffToAuto)); - //AddGenericPictureAction("superResolution", typeof(OffToAuto)); - AddGenericPictureAction("peakBrightness", typeof(OffToHigh), title: "Peak Brightness", fromModelYear: ModelYear.Series2019); - AddGenericPictureAction("smoothGradation", typeof(OffToAuto), title: "Smooth Gradation", fromModelYear: ModelYear.Series2019); - AddGenericPictureAction("energySaving", typeof(EnergySaving), title: "Energy Saving"); - AddGenericPictureAction("hdrDynamicToneMapping", typeof(DynamicTonemapping), title: "HDR Dynamic Tone Mapping"); - AddGenericPictureAction("blackLevel", typeof(BlackLevel), title: "HDMI Black Level"); - AddGenericPictureAction("dolbyPrecisionDetail", typeof(OffToOn), title: "Dolby Precision Detail", fromModelYear: ModelYear.Series2022); - - AddGenericPictureAction("ai_Brightness", typeof(OffToOn), title: "AI Brightness", fromModelYear: ModelYear.Series2019, category: "aiPicture"); - AddGenericPictureAction("ai_Genre", typeof(OffToOn), title: "AI Genre Selection", fromModelYear: ModelYear.Series2021, category: "aiPicture"); - AddGenericPictureAction("ai_Picture", typeof(OffToOn), title: "AI Picture Pro", fromModelYear: ModelYear.Series2021, category: "aiPicture"); - - AddGenericPictureAction("arcPerApp", typeof(AspectRatio), title: "Aspect Ratio", category: "aspectRatio"); - AddGenericPictureAction("justScan", typeof(OffToAuto2), title: "Just Scan", category: "aspectRatio"); - AddGenericPictureAction("allDirZoomHRatio", minValue: 0, maxValue: 10, title: "All-Direction Zoom Horizontal Ratio", category: "aspectRatio"); - AddGenericPictureAction("allDirZoomVRatio", minValue: 0, maxValue: 9, title: "All-Direction Zoom Vertical Ratio", category: "aspectRatio"); - AddGenericPictureAction("allDirZoomHPosition", minValue: -10, maxValue: 9, title: "All-Direction Zoom Horizontal Position", category: "aspectRatio"); - AddGenericPictureAction("allDirZoomVPosition", minValue: -9, maxValue: 9, title: "All-Direction Zoom Vertical Position", category: "aspectRatio"); - AddGenericPictureAction("vertZoomVPosition", minValue: -8, maxValue: 9, title: "Vertical Zoom Position", category: "aspectRatio"); - AddGenericPictureAction("vertZoomVRatio", minValue: 0, maxValue: 9, title: "Vertical Zoom Ratio", category: "aspectRatio"); - //AddGenericPictureAction("ambientLightCompensation", typeof(OffToAuto2)); - - AddGenericPictureAction("truMotionMode", typeof(TruMotionMode), title: "TruMotion"); - AddGenericPictureAction("truMotionJudder", minValue: 0, maxValue: 10, title: "TruMotion Judder"); - AddGenericPictureAction("truMotionBlur", minValue: 0, maxValue: 10, title: "TruMotion Blur"); - AddGenericPictureAction("motionProOLED", typeof(OffToHigh), title: "OLED Motion Pro", fromModelYear: ModelYear.Series2019); - AddGenericPictureAction("motionPro", typeof(OffToOn), title: "Motion Pro"); - AddGenericPictureAction("realCinema", typeof(OffToOn), title: "Real Cinema"); - - AddHdmiPictureAction("uhdDeepColorHDMI", typeof(OffToOn), category: "other"); - AddGenericPictureAction("lowLevelAdjustment", minValue: -30, maxValue: 30, category: "other", title: "Fine Tune Dark Areas", fromModelYear: ModelYear.Series2019); - AddGenericPictureAction("blackStabilizer", minValue: -30, maxValue: 30, category: "other", title: "Black Stabilizer", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("whiteStabilizer", minValue: -30, maxValue: 30, category: "other", title: "White Stabilizer", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("blueLight", typeof(OffToLevel2), category: "other", title: "Reduce Blue Light", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("darkMode", typeof(OffToLevel2), category: "other", title: "Dark room Mode", fromModelYear: ModelYear.Series2022); - AddGenericPictureAction("noiseReduction", typeof(OffToHigh), title: "Noise Reduction"); - AddGenericPictureAction("mpegNoiseReduction", typeof(OffToHigh), title: "MPEG Noise Reduction"); - - AddHdmiPictureAction("gameMode_hdmi", typeof(OffToOn), category: "other", title: "Game Optimizer HDMI1", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("gameOptimization", typeof(OffToOn), category: "other", title: "VRR & G-Sync", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("inputOptimization", typeof(InputOptimization), category: "other", title: "Prevent Input Delay(Input Lag)", fromModelYear: ModelYear.Series2021); - AddHdmiPictureAction("gameOptimizationHDMI", typeof(OffToOn), category: "other"); - AddGenericPictureAction("freesync", typeof(OffToOn), category: "other", title: "AMD FreeSync Premium", fromModelYear: ModelYear.Series2020); - AddHdmiPictureAction("freesyncOLEDHDMI", typeof(OffToOn), category: "other", fromModelYear: ModelYear.Series2020); - - AddHdmiPictureAction("444BypassHDMI", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through HDMIX", fromModelYear: ModelYear.Series2023); - AddGenericPictureAction("444BypassHDMINone", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through Non-HDMI", fromModelYear: ModelYear.Series2023); - AddGenericPictureAction("qmsVrr", typeof(OffToOn), category: "other", title: "QMS-VRR", fromModelYear: ModelYear.Series2023); - - AddGenericPictureAction("masterLuminanceLevel", typeof(MasterLuminanceLevel), category: "other"); - AddHdmiPictureAction("colorimetryHDMI", typeof(MasteringColor), category: "other", fromModelYear: ModelYear.Series2020); - AddHdmiPictureAction("masteringColorHDMI", typeof(MasteringColor), category: "other", fromModelYear: ModelYear.Series2020); - AddHdmiPictureAction("masteringPeakHDMI", typeof(MasteringNits), category: "other", fromModelYear: ModelYear.Series2020); - AddHdmiPictureAction("maxCLLHDMI", typeof(MasteringNits), category: "other", fromModelYear: ModelYear.Series2020); - AddHdmiPictureAction("maxFALLHDMI", typeof(MasteringNits), category: "other", fromModelYear: ModelYear.Series2020); - - AddHdmiPictureAction("hdmiPcMode_hdmi", typeof(FalseToTrue), category: "other"); - AddGenericPictureAction("adjustingLuminance", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); - AddGenericPictureAction("whiteBalanceBlue", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); - AddGenericPictureAction("whiteBalanceGreen", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); - AddGenericPictureAction("whiteBalanceRed", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); - AddGenericPictureAction("whiteBalanceBlue10pt", minValue: -50, maxValue: 50, numberOfValues: 10, valueLabels: Ire10ptValueLabels); - AddGenericPictureAction("whiteBalanceGreen10pt", minValue: -50, maxValue: 50, numberOfValues: 10, valueLabels: Ire10ptValueLabels); - AddGenericPictureAction("whiteBalanceRed10pt", minValue: -50, maxValue: 50, numberOfValues: 10, valueLabels: Ire10ptValueLabels); - AddGenericPictureAction("whiteBalanceBlueOffset", minValue: -50, maxValue: 50, title: "White Balance - Blue Offset"); - AddGenericPictureAction("whiteBalanceBlueGain", minValue: -50, maxValue: 50, title: "White Balance - Blue Gain"); - AddGenericPictureAction("whiteBalanceGreenOffset", minValue: -50, maxValue: 50, title: "White Balance - Green Offset"); - AddGenericPictureAction("whiteBalanceGreenGain", minValue: -50, maxValue: 50, title: "White Balance - Green Gain"); - AddGenericPictureAction("whiteBalanceRedOffset", minValue: -50, maxValue: 50, title: "White Balance - Red Offset"); - AddGenericPictureAction("whiteBalanceRedGain", minValue: -50, maxValue: 50, title: "White Balance - Red Gain"); - AddGenericPictureAction("whiteBalanceMethod", typeof(WhiteBalanceMethod), title: "White Balance - Method"); - AddInvokableAction("turnScreenOff", TurnScreenOffAction); - AddInvokableAction("turnScreenOn", TurnScreenOnAction); - - AddInternalPresetAction(new LgPreset("InStart", "com.webos.app.factorywin", new[] { "0", "4", "1", "3" }, new { id = "executeFactory", irKey = "inStart" })); - AddInternalPresetAction(new LgPreset("EzAdjust", "com.webos.app.factorywin", new[] { "0", "4", "1", "3" }, new { id = "executeFactory", irKey = "ezAdjust" })); - //AddInternalPresetAction(new LgPreset("PictureCheck", "com.webos.app.factorywin", null, new { id = "executeFactory", irKey = "pCheck" })); - AddInternalPresetAction(new LgPreset("Software Update", "com.webos.app.softwareupdate", null, new { mode = "user", flagUpdate = true })); - - AddSetDeviceConfigAction("HDMI_1_icon", typeof(HdmiIcon), "HDMI 1 icon"); - AddSetDeviceConfigAction("HDMI_2_icon", typeof(HdmiIcon), "HDMI 2 icon"); - AddSetDeviceConfigAction("HDMI_3_icon", typeof(HdmiIcon), "HDMI 3 icon"); - AddSetDeviceConfigAction("HDMI_4_icon", typeof(HdmiIcon), "HDMI 4 icon"); - - AddVolumeAction("audioVolume", "Volume"); - AddGenericPictureAction("soundMode", typeof(SoundMode), category: "sound", title: "Sound Mode"); - AddGenericPictureAction("smartSoundMode", typeof(OffToOn), category: "sound", title: "Adaptive Sound Control"); - AddGenericPictureAction("soundOutput", typeof(SoundOutput), category: "sound", title: "Sound Output"); - AddGenericPictureAction("autoVolume", typeof(OffToOn), category: "sound", title: "Auto Volume"); - AddGenericPictureAction("virtualSurround", typeof(OffToOn), category: "sound", title: "Virtual Surround"); - - AddGenericPictureAction("screenShift", typeof(OffToOn), title: "Screen Shift"); - AddGenericPictureAction("logoLuminanceAdjust", typeof(LogoLuminance), title: "Logo Luminance"); - - //AddGenericPictureAction("enableToastPopup", typeof(OffToOn), category: "option", title: "enableToastPopup"); - AddSetConfigAction("tv.conti.supportUsedTime", typeof(BoolFalseToTrue), title: "Total Power On Time"); - - AddGenericPictureAction("wolwowlOnOff", typeof(FalseToTrue), category: "network", title: "Wake-On-LAN"); - AddLunaAction("TPC", typeof(BoolFalseToTrue), title: "Temporal Peak Luminance Control (TPC)", ModelYear.Series2020); - AddLunaAction("GSR", typeof(BoolFalseToTrue), title: "Global Sticky/Stress Reduction (GSR)", ModelYear.Series2020); - } - - public LgDevice(LgDeviceDto device) : this(device.Name, device.IpAddress, device.MacAddress, device.IsCustom, device.IsDummy) - { - UpdateOptions(device.Options); - } - - private void UpdateOptions(LgDeviceOptions options) - { - UseSecureConnection = options.UseSecureConnection; - TurnScreenOffOnScreenSaver = options.TurnScreenOffOnScreenSaver; - TurnScreenOnAfterScreenSaver = options.TurnScreenOnAfterScreenSaver; - ScreenSaverMinimalDuration = options.ScreenSaverMinimalDuration; - HandleManualScreenSaver = options.HandleManualScreenSaver; - PowerOffOnScreenSaver = options.PowerOffOnScreenSaver; - PowerOnAfterScreenSaver = options.PowerOnAfterScreenSaver; - PowerOnAfterManualPowerOff = options.PowerOnAfterManualPowerOff; - PowerOnAfterResume = options.PowerOnAfterResume; - PowerOffOnStandby = options.PowerOffOnStandby; - PowerOffOnShutdown = options.PowerOffOnShutdown; - PowerOffByWindows = options.PowerOffByWindows; - PowerOnByWindows = options.PowerOnByWindows; - PowerOnAfterStartup = options.PowerOnAfterStartup; - PowerOnAfterSessionUnlock = options.PowerOnAfterSessionUnlock; - PowerOffAfterSessionLock = options.PowerOffAfterSessionLock; - TriggersEnabled = options.TriggersEnabled; - HDMIPortNumber = options.HDMIPortNumber; - } - - ~LgDevice() - { - ClearPowerOffTask(); - } - - public bool Update(LgDeviceDto device) - { - var reconnect = false; - - Name = device.Name; - if (IpAddress != device.IpAddress) - { - IpAddress = device.IpAddress; - reconnect = true; - } - MacAddress = device.MacAddress; - IsCustom = device.IsCustom; - IsDummy = device.IsDummy; - - UpdateOptions(device.Options); - - return reconnect; - } - - private void AddInvokableAction(string name, Func, Task> function) - { - var action = new InvokableAction - { - Name = name, - AsyncFunction = function, - Category = "misc", - Title = name - }; - - _invokableActions.Add(action); - } - - private void AddInternalPresetAction(LgPreset preset) - { - var action = new InvokableAction - { - Name = preset.name, - Preset = preset, - Advanced = true - }; - - _invokableActions.Add(action); - } - - private void AddHdmiPictureAction(string name, Type type = null, decimal minValue = 0, decimal maxValue = 0, string category = "picture", string title = null, int numberOfValues = 1, ModelYear fromModelYear = ModelYear.None) - { - for (var i = 1; i <= 4; i++) - { - var replacedTitle = title?.Replace("HDMIX", $"HDMI{i}"); - - AddGenericPictureAction($"{name}{i}", type, minValue, maxValue, category, replacedTitle, numberOfValues, fromModelYear); - } - } - - private void AddGenericPictureAction(string name, Type type = null, decimal minValue = 0, decimal maxValue = 0, string category = "picture", string title = null, int numberOfValues = 1, ModelYear fromModelYear = ModelYear.None, List valueLabels = null) - { - var action = new InvokableAction - { - Name = name, - AsyncFunction = GenericPictureAction, - EnumType = type, - MinValue = minValue, - MaxValue = maxValue, - NumberOfValues = numberOfValues, - Category = category, - Title = title == null ? Utils.FirstCharUpperCase(name) : title, - FromModelYear = fromModelYear, - ValueLabels = valueLabels - }; - - _invokableActions.Add(action); - } - - private void AddSetDeviceConfigAction(string name, Type type, string title) - { - var action = new InvokableAction - { - Name = name, - AsyncFunction = GenericDeviceConfigAction, - EnumType = type, - Title = title == null ? Utils.FirstCharUpperCase(name) : title - }; - - _invokableActions.Add(action); - } - - private void AddSetConfigAction(string name, Type type, string title) - { - var action = new InvokableAction - { - Name = name, - AsyncFunction = GenericSetConfigAction, - EnumType = type, - Title = title == null ? Utils.FirstCharUpperCase(name) : title, - Category = "Config", - Advanced = true - }; - - _invokableActions.Add(action); - } - - private void AddLunaAction(string name, Type type, string title, ModelYear fromModelYear = ModelYear.None) - { - var action = new InvokableAction - { - Name = name, - AsyncFunction = GenericLunaAction, - EnumType = type, - Title = title == null ? Utils.FirstCharUpperCase(name) : title, - Category = "Config", - FromModelYear = fromModelYear, - Advanced = true - }; - - _invokableActions.Add(action); - } - - private void AddVolumeAction(string name, string title, ModelYear fromModelYear = ModelYear.None) - { - var action = new InvokableAction - { - Name = name, - AsyncFunction = GenericVolumeAction, - MinValue = 0, - MaxValue = 100, - Title = title == null ? Utils.FirstCharUpperCase(name) : title, - Category = "sound", - FromModelYear = fromModelYear - }; - - _invokableActions.Add(action); - } - - public void AddGameBarAction(string name) - { - if (!ActionsOnGameBar.Contains(name)) - { - ActionsOnGameBar.Add(name); - } - } - - public void RemoveGameBarAction(string name) - { - ActionsOnGameBar.Remove(name); - } - - public override string ToString() - { - //return (IsDummy ? string.Empty : (IsCustom ? "Custom: " : "Auto detect: ")) + $"{Name}" + (!string.IsNullOrEmpty(IpAddress) ? $" ({IpAddress})" : string.Empty); - return $"{(IsDummy ? string.Empty : (IsCustom ? "Custom: " : "Auto detect: "))}{Name}{(!string.IsNullOrEmpty(IpAddress) ? ", " + IpAddress : string.Empty)}"; - } - - public async Task Connect(int retries = 3) - { - var locked = _connectSemaphore.CurrentCount == 0; - await _connectSemaphore.WaitAsync(); - try - { - if (locked && _lgTvApi != null) - { - return true; - } - - try - { - DisposeConnection(); - _lgTvApi = await LgTvApi.CreateLgTvApi(IpAddress, retries, UseSecureConnection); - - //Test(); - //_lgTvApi.Test3(); - if (_lgTvApi != null && !Utils.ConsoleOpened) - { - var info = await _lgTvApi.GetSystemInfo("modelName"); - if (info != null) - { - ModelName = info.modelName; - SetModelYear(); - } - - Logger.Debug($"LG TV with ip-address {IpAddress} has model name {ModelName}"); - - await _lgTvApi.SubscribeVolume(VolumeChanged); - await _lgTvApi.SubscribePowerState(PowerStateChanged); - await _lgTvApi.SubscribePictureSettings(PictureSettingsChanged); - await _lgTvApi.SubscribeForegroundApp(ForegroundAppChanged); - - //await _lgTvApi.SetConfig("tv.conti.supportUsedTime", true); - - //await _lgTvApi.Reboot(); - - //var result = await GetPictureSettings(); - - //await _lgTvApi.SetSystemSettings("adjustingLuminance", new[] { 0, 0, -5, -10, -15, -20, -25, -30, -35, -40, -45, -50, -50, -50, -40, -30, -20, -10, 10, 20, 30, 50 }); - //await _lgTvApi.SetSystemSettings("adjustingLuminance", new[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }); - - //await _lgTvApi.SetInput("HDMI_1"); - //await Task.Delay(2000); - //await _lgTvApi.SetConfig("com.palm.app.settings.enableHdmiPcLabel", true); - //await _lgTvApi.SetInput("HDMI_2"); - } - return _lgTvApi != null; - } - catch (Exception ex) - { - string logMessage = ex.ToLogString(Environment.StackTrace); - Logger.Error($"Error while connecting to {IpAddress}: {logMessage}"); - return false; - } - } - finally - { - _connectSemaphore.Release(); - } - } - - private void SetModelYear() - { - if (ModelName == null || ModelName.Contains("OLED") != true) - { - Year = ModelYear.None; - return; - } - - var normalized = ModelName.Replace("OLED", "").Replace(" ", ""); - - if (normalized.Length > 3) - { - normalized = normalized.Substring(3); - } - - var suffix = normalized.First(); - - Year = suffix switch - { - '6' => ModelYear.SeriesPre2018, - '7' => ModelYear.SeriesPre2018, - '8' => ModelYear.Series2018, - '9' => ModelYear.Series2019, - 'X' => ModelYear.Series2020, - '1' => ModelYear.Series2021, - '2' => ModelYear.Series2022, - '3' => ModelYear.Series2023, - _ => ModelYear.None - }; - - _invokableActions = _invokableActions.Where(a => Year == ModelYear.None || a.FromModelYear == ModelYear.None || Year >= a.FromModelYear).ToList(); - } - - public bool VolumeChanged(dynamic payload) - { - if (payload.volume != null) - { - PictureSettings.Volume = Utils.ParseDynamicAsInt(payload.volume, PictureSettings.Volume); - } - - var volumeAction = _invokableActions.FirstOrDefault(a => a.Name == "audioVolume"); - - if (volumeAction != null) - { - volumeAction.CurrentValue = PictureSettings.Volume; - } - - PictureSettingsChangedEvent?.Invoke(this, EventArgs.Empty); - - return true; - } - - public bool PictureSettingsChanged(dynamic payload) - { - if (payload.settings != null) - { - var settings = payload.settings; - - Logger.Debug($"[{Name}] PictureSettingsChanged: {JsonConvert.SerializeObject(settings)}"); - - if (settings.backlight != null) - { - PictureSettings.Backlight = Utils.ParseDynamicAsInt(settings.backlight, PictureSettings.Backlight); - } - if (settings.contrast != null) - { - PictureSettings.Contrast = Utils.ParseDynamicAsInt(settings.contrast, PictureSettings.Contrast); - } - if (settings.brightness != null) - { - PictureSettings.Brightness = Utils.ParseDynamicAsInt(settings.brightness, PictureSettings.Brightness); - } - if (settings.color != null) - { - PictureSettings.Color = Utils.ParseDynamicAsInt(settings.color, PictureSettings.Color); - } - } - - PictureSettingsChangedEvent?.Invoke(this, EventArgs.Empty); - - return true; - } - - public bool PowerStateChanged(dynamic payload) - { - Logger.Debug($"[{Name}] Power state change: {JsonConvert.SerializeObject(payload)}"); - - var state = payload.state != null ? ((string)payload.state).Replace(' ', '_') : PowerState.Unknown.ToString(); - - PowerState newState; - if (Enum.TryParse(state, out newState)) - { - CurrentState = newState; - - if (CurrentState == PowerState.Active) - { - if (payload.processing == null && (DateTimeOffset.Now - _poweredOffViaAppDateTime).TotalMilliseconds > 500) - { - PoweredOffViaApp = false; - PoweredOffBy = PowerOffSource.Unknown; - _poweredOffViaAppDateTime = DateTimeOffset.MinValue; - } - else if (payload.processing != null && ((string)payload.processing).Equals("Request Power Off", StringComparison.Ordinal)) - { - PoweredOffBy = PoweredOffViaApp ? PowerOffSource.App : PowerOffSource.Manually; - } - } - else if (CurrentState != PowerState.Screen_Off) - { - PoweredOffBy = PoweredOffViaApp ? PowerOffSource.App : PowerOffSource.Manually; - } - } - else - { - CurrentState = PowerState.Unknown; - Logger.Warn($"Unknown power state: {state}"); - } - - Logger.Debug($"PoweredOffBy: {PoweredOffBy}, PoweredOffViaApp: {PoweredOffViaApp}"); - - PowerStateChangedEvent?.Invoke(this, EventArgs.Empty); - - return true; - } - - public bool ForegroundAppChanged(dynamic payload) - { - Logger.Debug($"[{Name}] ForegroundAppChanged: {JsonConvert.SerializeObject(payload)}"); - - if (payload.appId != null) - { - CurrentAppId = payload.appId; - } - - return true; - } - - public bool IsConnected() - { - return !_lgTvApi?.ConnectionClosed ?? false; - } - - private void DisposeConnection() - { - if (_lgTvApi != null) - { - _lgTvApi.Dispose(); - _lgTvApi = null; - } - } - - public async Task ExecutePreset(LgPreset preset, bool reconnect, LgServiceConfig config) - { - var hasApp = !string.IsNullOrEmpty(preset.appId); - - var hasWOL = preset.steps.Any(s => s.Equals("WOL", StringComparison.OrdinalIgnoreCase)); - - if (hasWOL) - { - var connected = await WakeAndConnect(0); - if (!connected) - { - return false; - } - } - - for (var tries = 0; tries <= 1; tries++) - { - if (!await Connected(reconnect || tries == 1)) - { - return false; - } - - if (hasApp) - { - try - { - var @params = preset.AppParams; - if (@params == null && config.ShowAdvancedActions) - { - if (preset.appId.Equals("com.webos.app.softwareupdate")) - { - @params = new { mode = "user", flagUpdate = true }; - } - else if (preset.appId.Equals("com.webos.app.factorywin")) - { - if (preset.name.Contains("ezadjust", StringComparison.OrdinalIgnoreCase)) - { - @params = new { id = "executeFactory", irKey = "ezAdjust" }; - } - else - { - @params = new { id = "executeFactory", irKey = "inStart" }; - } - } - } - - await _lgTvApi.LaunchApp(preset.appId, @params); - } - catch (Exception ex) - { - string logMessage = ex.ToLogString(Environment.StackTrace); - Logger.Error("Error while launching app: " + logMessage); - - if (tries == 0) - { - continue; - } - return false; - } - - if (_justWokeUp) - { - _justWokeUp = false; - await Task.Delay(1000); - } - } - - if (preset.steps.Any()) - { - if (hasApp) - { - await Task.Delay(1500); - } - try - { - await ExecuteSteps(_lgTvApi, preset, config); - } - catch (Exception ex) - { - string logMessage = ex.ToLogString(Environment.StackTrace); - Logger.Error("Error while executing steps: " + logMessage); - - if (tries == 0) - { - continue; - } - return false; - } - } - - return true; - } - - return true; - } - - private async Task ExecuteSteps(LgTvApi api, LgPreset preset, LgServiceConfig config) - { - LgWebOsMouseService mouse = null; - - foreach (var step in preset.steps) - { - var keySpec = step.Split(':'); - - var delay = 10; - var key = step; - if (keySpec.Length == 2) - { - delay = Utils.ParseInt(keySpec[1]); - if (delay > 0) - { - key = keySpec[0]; - } - } - - var index = key.IndexOf("("); - string[] parameters = null; - if (index > -1) - { - var keyValue = key.Split('('); - key = keyValue[0]; - parameters = keyValue[1].Substring(0, keyValue[1].Length - 1).Split(';'); - } - - var executeKey = true; - var action = _invokableActions.FirstOrDefault(a => a.Name.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (action != null) - { - await ExecuteActionAsync(action, parameters); - - executeKey = false; - } - if (parameters != null) - { - _serviceManager ??= Program.ServiceProvider.GetRequiredService(); - - if (await _serviceManager.HandleExternalServiceAsync(key, parameters)) - { - executeKey = false; - } - } - - if (executeKey) - { - mouse ??= await api.GetMouse(); - await SendKey(mouse, key); - delay = delay <= 10 ? config.DefaultButtonDelay : delay; - } - - if (delay > 0) - { - await Task.Delay(delay); - } - } - } - - public async Task ExecuteActionAsync(InvokableAction action, string[] parameters) - { - var function = action.AsyncFunction; - if (function == null) - { - return; - } - - if (parameters?.Length > 0) - { - var keyValues = new Dictionary { - { "name", action.Name }, - { "value", parameters }, - { "category", action.Category } - }; - - await function(keyValues); - return; - } - - await function(null); - } - - private async Task SendKey(LgWebOsMouseService mouse, string key) - { - key = key.ToUpper(); - if (key.Length >= 1 && int.TryParse(key[0].ToString(), out _)) - { - key = "_" + key; - } - var button = (ButtonType)Enum.Parse(typeof(ButtonType), key); - await mouse.SendButton(button); - } - - public async Task GetMouseAsync() - { - await CheckConnectionAsync(); - - return await _lgTvApi.GetMouse(); - } - - private async Task CheckConnectionAsync() - { - if (!await Connected()) - { - throw new InvalidOperationException("Not connected"); - } - } - - public async Task> GetApps(bool force = false) - { - if (!force) - { - await Task.Delay(5000); - } - - if (!await Connected(force)) - { - Logger.Debug("Cannot refresh apps: no connection could be made"); - return new List(); - } - - return await _lgTvApi.GetApps(force); - } - - public async Task PerformActionOnScreenSaver() - { - if (!IsPowerOffAllowed()) - { - return true; - } - - if (TurnScreenOffOnScreenSaver) - { - Logger.Debug("Turning screen off on screen saver"); - await _lgTvApi.TurnScreenOff(); - - return true; - } - - Logger.Debug($"Device {Name} is now powering off due to delayed screensaver task"); - - return await PowerOff(true, false); - } - - public async Task PerformActionAfterScreenSaver(int powerOnRetries) - { - if (TurnScreenOffOnScreenSaver && IsConnected()) - { - if (TurnScreenOnAfterScreenSaver) - { - try - { - Logger.Debug("Turning screen on after screen saver"); - await _lgTvApi.TurnScreenOn(); - } - catch (Exception) { } - } - - return true; - } - - return await WakeAndConnectWithRetries(powerOnRetries); - } - - internal async Task PowerOff(bool checkHdmi = false, bool reconnect = true) - { - if (!await Connected(reconnect) || CurrentState != PowerState.Active) - { - return false; - } - - if (checkHdmi && !IsPowerOffAllowed()) - { - return true; - } - - PoweredOffViaApp = true; - _poweredOffViaAppDateTime = DateTimeOffset.Now; - - try - { - await _lgTvApi.TurnOff().WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(true); - } - catch (TimeoutException ex) - { - Logger.Debug($"Timeout when turning off tv: {ex.Message}"); - } - - return true; - } - - public bool IsPowerOffAllowed() - { - if (CurrentAppId != null && HDMIPortNumber != 0 && !CurrentAppId.EndsWith($".hdmi{HDMIPortNumber}", StringComparison.InvariantCulture)) - { - Logger.Debug($"[{Name}] PowerOff/ScreenOff is ignored because current app {CurrentAppId} does not match configured HDMI port {HDMIPortNumber}"); - return false; - } - - return true; - } - - internal async Task TestConnection(int retries = 1) - { - if (!await Connected(true, retries)) - { - return false; - } - - try - { - await _lgTvApi.IsMuted(); - return true; - } - catch (Exception ex) - { - Logger.Error("TestConnection: " + ex.ToLogString()); - return false; - } - } - - internal async Task WakeAndConnect(int wakeDelay = 5000, int connectDelay = 500) - { - try - { - await Task.Delay(wakeDelay); - var result = Wake(); - if (!result) - { - Logger.Debug("WOL failed"); - return false; - } - Logger.Debug("WOL succeeded"); - await Task.Delay(connectDelay); - result = await Connect(1); - Logger.Debug("Connect succeeded: " + result); - return result; - } - catch (Exception e) - { - Logger.Error("WakeAndConnectToSelectedDevice: " + e.ToLogString()); - return false; - } - } - - internal bool Wake() - { - var result = false; - - if (MacAddress != null) - { - var wolService = Program.ServiceProvider.GetRequiredService(); - - result = wolService.SendWol(MacAddress, IpAddress); - _justWokeUp = true; - } - else - { - Logger.Debug("Cannot wake device: the device has no MAC-address"); - } - - return result; - } - - internal async Task WakeAndConnectWithRetries(int retries = 5) - { - var wakeDelay = 0; - var maxRetries = retries <= 1 ? 5 : retries; - - var result = false; - for (var retry = 0; retry < maxRetries && !result; retry++) - { - Logger.Debug($"WakeAndConnectWithRetries: attempt {retry + 1} of {maxRetries}..."); - - var ms = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - result = await WakeAndConnect(retry == 0 ? wakeDelay : 0); - ms = DateTimeOffset.Now.ToUnixTimeMilliseconds() - ms; - if (!result) - { - var delay = 2000 - ms; - if (delay > 0) - { - await Task.Delay((int)delay); - } - } - } - - return result; - } - - public List GetInvokableActions(bool includedAdvanced = false) - { - return _invokableActions.Where(a => includedAdvanced || !a.Advanced).ToList(); - } - - public List GetInvokableActionsForGameBar() - { - var actions = GetInvokableActions(); - return actions.Where(a => - a.Category == "picture" && (a.EnumType != null && a.EnumType != typeof(PictureMode) || a.MinValue >= 0 && a.MaxValue > 0) || - a.Name == "audioVolume" || (a.Category == "other" && (a.MinValue < a.MaxValue || a.EnumType != null)) - ) - .ToList(); - } - - public List GetActionsForGameBar() - { - var actions = GetInvokableActions(); - return actions.Where(a => ActionsOnGameBar.Contains(a.Name)).ToList(); - } - - public void Test() - { - try - { - //_lgTvApi.GetServiceList(); - //_lgTvApi?.Test(); - } - catch (Exception ex) - { - Logger.Error("TEST: " + ex.ToLogString()); - } - } - - private async Task Connected(bool reconnect = false, int retries = 3) - { - if (reconnect || !IsConnected() || !string.Equals(_lgTvApi.GetIpAddress(), IpAddress)) - { - if (!await Connect(retries)) - { - Logger.Debug("Cannot apply LG-preset: no connection could be made"); - return false; - } - } - return true; - } - - internal void ConvertToCustom() - { - IsCustom = true; - } - - public bool IsUsingHDRPictureMode() - { - // Temporary workaround because I cannot read the picture mode at this time - return PictureSettings.Contrast == 100; - } - - internal async Task SetBacklight(int backlight) - { - await CheckConnectionAsync(); - - await _lgTvApi.SetSystemSettings("backlight", backlight.ToString()); - } - - internal async Task SetContrast(int contrast) - { - await CheckConnectionAsync(); - - await _lgTvApi.SetSystemSettings("contrast", contrast.ToString()); - } - - public async Task SetOLEDMotionPro(string mode) - { - await CheckConnectionAsync(); - - await _lgTvApi.SetConfig("tv.model.motionProMode", mode); - } - - public async Task SetSvcMenuFlag(bool enabled) - { - await CheckConnectionAsync(); - - await _lgTvApi.SetSystemSettings("svcMenuFlag", enabled, "other"); - } - - internal async Task SetConfig(string key, object value) - { - await CheckConnectionAsync(); - - await _lgTvApi.SetConfig(key, value); - } - internal async Task SetSystemSettings(string name, string value) - { - await CheckConnectionAsync(); - - await _lgTvApi.SetSystemSettings(name, value); - - UpdateCurrentValueOfAction(name, value); - } - - internal async Task GetPictureSettings() - { - //var keys = new[] { "backlight", "brightness", "contrast", "color", "pictureMode", "colorGamut", "dynamicContrast", "peakBrightness", "smoothGradation", "energySaving", "motionProOLED" }; - //var keys = new[] { "backlight", "brightness", "contrast", "color" }; - - await CheckConnectionAsync(); - - return await _lgTvApi.GetSystemSettings2("picture"); - } + class LgDevice + { + public class InvokableAction + { + public Func, Task> AsyncFunction { get; set; } + public string Name { get; set; } + public Type EnumType { get; set; } + public decimal MinValue { get; set; } + public decimal MaxValue { get; set; } + public string Category { get; set; } + public string Title { get; set; } + public int CurrentValue { get; set; } + public int NumberOfValues { get; set; } + public LgPreset Preset { get; set; } + public bool Advanced { get; set; } + public ModelYear FromModelYear { get; set; } = ModelYear.None; + public ModelYear ToModelYear { get; set; } = ModelYear.None; + public List ValueLabels { get; set; } + } + + public class LgDevicePictureSettings + { + public int Backlight { get; set; } + public int Contrast { get; set; } + public int Brightness { get; set; } + public int Color { get; set; } + public int Volume { get; set; } + } + + public enum PowerState + { + Unknown, + Active, + Power_Off, + Suspend, + Active_Standby, + Screen_Off + } + + public enum PowerOffSource + { + Unknown, + App, + Manually + } + + public enum ModelYear + { + None = 0, + SeriesPre2018, + Series2018, + Series2019, + Series2020, + Series2021, + Series2022, + Series2023 + } + + protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + public static List DefaultActionsOnGameBar = new() { "backlight", "contrast", "brightness", "color" }; + + private static List IreValueLabels = ["IRE 2.5", "IRE 5", "IRE 7.5", "IRE 10", "IRE 15", "IRE 20", "IRE 25", "IRE 30", "IRE 35", "IRE 40", "IRE 45", "IRE 50", "IRE 55", "IRE 60", "IRE 65", "IRE 70", "IRE 75", "IRE 80", "IRE 85", "IRE 90", "IRE 95", "IRE 100"]; + private static List Ire10ptValueLabels = ["IRE 10", "IRE 20", "IRE 30", "IRE 40", "IRE 50", "IRE 60", "IRE 70", "IRE 80", "IRE 90", "IRE 100"]; + + public string Name { get; private set; } + public string IpAddress { get; private set; } + public string MacAddress { get; private set; } + public bool IsCustom { get; private set; } + [JsonIgnore] + public bool IsDummy { get; private set; } + + public bool PowerOnAfterStartup { get; set; } + public bool PowerOnAfterResume { get; set; } + public bool PowerOffOnShutdown { get; set; } + public bool PowerOffOnStandby { get; set; } + public bool PowerOffOnScreenSaver { get; set; } + public int ScreenSaverMinimalDuration { get; set; } + public bool TurnScreenOffOnScreenSaver { get; set; } + public bool HandleManualScreenSaver { get; set; } + public bool PowerOnAfterScreenSaver { get; set; } + public bool TurnScreenOnAfterScreenSaver { get; set; } + public bool PowerOnAfterManualPowerOff { get; set; } + public bool PowerOnByWindows { get; set; } + public bool PowerOffByWindows { get; set; } + public bool PowerOnAfterSessionUnlock { get; set; } + public bool PowerOffAfterSessionLock { get; set; } + public bool TriggersEnabled { get; set; } + public int HDMIPortNumber { get; set; } + + public bool UseSecureConnection { get; set; } = true; + + private List _actionsForGameBar; + + public List ActionsOnGameBar + { + get + { + return _actionsForGameBar; + } + set + { + if (value?.Any() == false) + { + _actionsForGameBar = DefaultActionsOnGameBar; + } + else + { + _actionsForGameBar = value; + } + } + } + + [JsonIgnore] + public PowerState CurrentState { get; set; } + + [JsonIgnore] + private LgTvApi _lgTvApi; + [JsonIgnore] + private bool _justWokeUp; + [JsonIgnore] + public PowerOffSource PoweredOffBy { get; private set; } + + [JsonIgnore] + public bool PoweredOffViaApp { get; private set; } + [JsonIgnore] + private DateTimeOffset _poweredOffViaAppDateTime { get; set; } + [JsonIgnore] + private List _invokableActions = new List(); + [JsonIgnore] + private SemaphoreSlim _connectSemaphore = new SemaphoreSlim(1, 1); + + [JsonIgnore] + public string ModelName { get; private set; } + [JsonIgnore] + public ModelYear Year { get; private set; } + + [JsonIgnore] + public LgDevicePictureSettings PictureSettings { get; private set; } + + [JsonIgnore] + public string CurrentAppId { get; private set; } + + public event EventHandler PictureSettingsChangedEvent; + public event EventHandler PowerStateChangedEvent; + + [JsonIgnore] + private Timer _powerOffTimer; + + [JsonIgnore] + private ServiceManager _serviceManager; + + [JsonConstructor] + public LgDevice(string name, string ipAddress, string macAddress, bool isCustom = true, bool isDummy = false) + { + PictureSettings = new LgDevicePictureSettings(); + ActionsOnGameBar = new List(DefaultActionsOnGameBar); + + Name = name; + IpAddress = ipAddress; + MacAddress = macAddress; + IsCustom = isCustom; + IsDummy = isDummy; + TriggersEnabled = true; + + AddInvokableAction("WOL", WakeAction); + AddInvokableAction("Reboot", RebootAction); + AddGenericPictureAction("backlight", minValue: 0, maxValue: 100); + AddGenericPictureAction("brightness", minValue: 0, maxValue: 100, title: "Brightness/Black Level"); + AddGenericPictureAction("contrast", minValue: 0, maxValue: 100); + AddGenericPictureAction("color", minValue: 0, maxValue: 100); + AddGenericPictureAction("pictureMode", typeof(PictureMode), title: "Picture Mode"); + AddGenericPictureAction("sharpness", minValue: 0, maxValue: 50); + //AddGenericPictureAction("dynamicRange", typeof(DynamicRange), category: "dimensionInfo", title: "Dynamic Range"); + AddGenericPictureAction("colorGamut", typeof(ColorGamut), title: "Color Gamut"); + AddGenericPictureAction("dynamicContrast", typeof(OffToHigh), title: "Dynamic Contrast"); + AddGenericPictureAction("superResolution", typeof(OffToHigh), title: "Super Resolution"); + AddGenericPictureAction("gamma", typeof(GammaExp)); + AddGenericPictureAction("colorTemperature", minValue: -50, maxValue: 50, title: "Color Temperature"); + AddGenericPictureAction("whiteBalanceColorTemperature", typeof(WhiteBalanceColorTemperature), title: "White Balance Color Temperature"); + AddGenericPictureAction("eyeComfortMode", typeof(OffToOn), title: "Eye Comfort Mode"); + //AddGenericPictureAction("dynamicColor", typeof(OffToAuto)); + //AddGenericPictureAction("superResolution", typeof(OffToAuto)); + AddGenericPictureAction("peakBrightness", typeof(OffToHigh), title: "Peak Brightness", fromModelYear: ModelYear.Series2019); + AddGenericPictureAction("smoothGradation", typeof(OffToAuto), title: "Smooth Gradation", fromModelYear: ModelYear.Series2019); + AddGenericPictureAction("energySaving", typeof(EnergySaving), title: "Energy Saving"); + AddGenericPictureAction("hdrDynamicToneMapping", typeof(DynamicTonemapping), title: "HDR Dynamic Tone Mapping"); + AddGenericPictureAction("blackLevel", typeof(BlackLevel), title: "HDMI Black Level"); + AddGenericPictureAction("dolbyPrecisionDetail", typeof(OffToOn), title: "Dolby Precision Detail", fromModelYear: ModelYear.Series2022); + + AddGenericPictureAction("ai_Brightness", typeof(OffToOn), title: "AI Brightness", fromModelYear: ModelYear.Series2019, category: "aiPicture"); + AddGenericPictureAction("ai_Genre", typeof(OffToOn), title: "AI Genre Selection", fromModelYear: ModelYear.Series2021, category: "aiPicture"); + AddGenericPictureAction("ai_Picture", typeof(OffToOn), title: "AI Picture Pro", fromModelYear: ModelYear.Series2021, category: "aiPicture"); + + AddGenericPictureAction("arcPerApp", typeof(AspectRatio), title: "Aspect Ratio", category: "aspectRatio"); + AddGenericPictureAction("justScan", typeof(OffToAuto2), title: "Just Scan", category: "aspectRatio"); + AddGenericPictureAction("allDirZoomHRatio", minValue: 0, maxValue: 10, title: "All-Direction Zoom Horizontal Ratio", category: "aspectRatio"); + AddGenericPictureAction("allDirZoomVRatio", minValue: 0, maxValue: 9, title: "All-Direction Zoom Vertical Ratio", category: "aspectRatio"); + AddGenericPictureAction("allDirZoomHPosition", minValue: -10, maxValue: 9, title: "All-Direction Zoom Horizontal Position", category: "aspectRatio"); + AddGenericPictureAction("allDirZoomVPosition", minValue: -9, maxValue: 9, title: "All-Direction Zoom Vertical Position", category: "aspectRatio"); + AddGenericPictureAction("vertZoomVPosition", minValue: -8, maxValue: 9, title: "Vertical Zoom Position", category: "aspectRatio"); + AddGenericPictureAction("vertZoomVRatio", minValue: 0, maxValue: 9, title: "Vertical Zoom Ratio", category: "aspectRatio"); + //AddGenericPictureAction("ambientLightCompensation", typeof(OffToAuto2)); + + AddGenericPictureAction("truMotionMode", typeof(TruMotionMode), title: "TruMotion"); + AddGenericPictureAction("truMotionJudder", minValue: 0, maxValue: 10, title: "TruMotion Judder"); + AddGenericPictureAction("truMotionBlur", minValue: 0, maxValue: 10, title: "TruMotion Blur"); + AddGenericPictureAction("motionProOLED", typeof(OffToHigh), title: "OLED Motion Pro", fromModelYear: ModelYear.Series2019); + AddGenericPictureAction("motionPro", typeof(OffToOn), title: "Motion Pro"); + AddGenericPictureAction("realCinema", typeof(OffToOn), title: "Real Cinema"); + + AddHdmiPictureAction("uhdDeepColorHDMI", typeof(OffToOn), category: "other"); + AddGenericPictureAction("lowLevelAdjustment", minValue: -30, maxValue: 30, category: "other", title: "Fine Tune Dark Areas", fromModelYear: ModelYear.Series2019); + AddGenericPictureAction("blackStabilizer", minValue: -30, maxValue: 30, category: "other", title: "Black Stabilizer", fromModelYear: ModelYear.Series2021); + AddGenericPictureAction("whiteStabilizer", minValue: -30, maxValue: 30, category: "other", title: "White Stabilizer", fromModelYear: ModelYear.Series2021); + AddGenericPictureAction("blueLight", typeof(OffToLevel2), category: "other", title: "Reduce Blue Light", fromModelYear: ModelYear.Series2021); + AddGenericPictureAction("darkMode", typeof(OffToLevel2), category: "other", title: "Dark room Mode", fromModelYear: ModelYear.Series2022); + AddGenericPictureAction("noiseReduction", typeof(OffToHigh), title: "Noise Reduction"); + AddGenericPictureAction("mpegNoiseReduction", typeof(OffToHigh), title: "MPEG Noise Reduction"); + + AddHdmiPictureAction("gameMode_hdmi", typeof(OffToOn), category: "other", title: "Game Optimizer HDMI1", fromModelYear: ModelYear.Series2021); + AddGenericPictureAction("gameOptimization", typeof(OffToOn), category: "other", title: "VRR & G-Sync", fromModelYear: ModelYear.Series2021); + AddGenericPictureAction("inputOptimization", typeof(InputOptimization), category: "other", title: "Prevent Input Delay(Input Lag)", fromModelYear: ModelYear.Series2021); + AddHdmiPictureAction("gameOptimizationHDMI", typeof(OffToOn), category: "other"); + AddGenericPictureAction("freesync", typeof(OffToOn), category: "other", title: "AMD FreeSync Premium", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("freesyncOLEDHDMI", typeof(OffToOn), category: "other", fromModelYear: ModelYear.Series2020); + + AddHdmiPictureAction("444BypassHDMI", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through HDMIX", fromModelYear: ModelYear.Series2023); + AddGenericPictureAction("444BypassHDMINone", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through Non-HDMI", fromModelYear: ModelYear.Series2023); + AddGenericPictureAction("qmsVrr", typeof(OffToOn), category: "other", title: "QMS-VRR", fromModelYear: ModelYear.Series2023); + + AddGenericPictureAction("masterLuminanceLevel", typeof(MasterLuminanceLevel), category: "other"); + AddHdmiPictureAction("colorimetryHDMI", typeof(MasteringColor), category: "other", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("masteringColorHDMI", typeof(MasteringColor), category: "other", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("masteringPeakHDMI", typeof(MasteringNits), category: "other", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("maxCLLHDMI", typeof(MasteringNits), category: "other", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("maxFALLHDMI", typeof(MasteringNits), category: "other", fromModelYear: ModelYear.Series2020); + + AddHdmiPictureAction("hdmiPcMode_hdmi", typeof(FalseToTrue), category: "other"); + AddGenericPictureAction("adjustingLuminance", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); + AddGenericPictureAction("whiteBalanceBlue", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); + AddGenericPictureAction("whiteBalanceGreen", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); + AddGenericPictureAction("whiteBalanceRed", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); + AddGenericPictureAction("whiteBalanceBlue10pt", minValue: -50, maxValue: 50, numberOfValues: 10, valueLabels: Ire10ptValueLabels); + AddGenericPictureAction("whiteBalanceGreen10pt", minValue: -50, maxValue: 50, numberOfValues: 10, valueLabels: Ire10ptValueLabels); + AddGenericPictureAction("whiteBalanceRed10pt", minValue: -50, maxValue: 50, numberOfValues: 10, valueLabels: Ire10ptValueLabels); + AddGenericPictureAction("whiteBalanceBlueOffset", minValue: -50, maxValue: 50, title: "White Balance - Blue Offset"); + AddGenericPictureAction("whiteBalanceBlueGain", minValue: -50, maxValue: 50, title: "White Balance - Blue Gain"); + AddGenericPictureAction("whiteBalanceGreenOffset", minValue: -50, maxValue: 50, title: "White Balance - Green Offset"); + AddGenericPictureAction("whiteBalanceGreenGain", minValue: -50, maxValue: 50, title: "White Balance - Green Gain"); + AddGenericPictureAction("whiteBalanceRedOffset", minValue: -50, maxValue: 50, title: "White Balance - Red Offset"); + AddGenericPictureAction("whiteBalanceRedGain", minValue: -50, maxValue: 50, title: "White Balance - Red Gain"); + AddGenericPictureAction("whiteBalanceMethod", typeof(WhiteBalanceMethod), title: "White Balance - Method"); + AddInvokableAction("turnScreenOff", TurnScreenOffAction); + AddInvokableAction("turnScreenOn", TurnScreenOnAction); + + AddInternalPresetAction(new LgPreset("InStart", "com.webos.app.factorywin", new[] { "0", "4", "1", "3" }, new { id = "executeFactory", irKey = "inStart" })); + AddInternalPresetAction(new LgPreset("EzAdjust", "com.webos.app.factorywin", new[] { "0", "4", "1", "3" }, new { id = "executeFactory", irKey = "ezAdjust" })); + //AddInternalPresetAction(new LgPreset("PictureCheck", "com.webos.app.factorywin", null, new { id = "executeFactory", irKey = "pCheck" })); + AddInternalPresetAction(new LgPreset("Software Update", "com.webos.app.softwareupdate", null, new { mode = "user", flagUpdate = true })); + + AddSetDeviceConfigAction("HDMI_1_icon", typeof(HdmiIcon), "HDMI 1 icon"); + AddSetDeviceConfigAction("HDMI_2_icon", typeof(HdmiIcon), "HDMI 2 icon"); + AddSetDeviceConfigAction("HDMI_3_icon", typeof(HdmiIcon), "HDMI 3 icon"); + AddSetDeviceConfigAction("HDMI_4_icon", typeof(HdmiIcon), "HDMI 4 icon"); + + AddVolumeAction("audioVolume", "Volume"); + AddGenericPictureAction("soundMode", typeof(SoundMode), category: "sound", title: "Sound Mode"); + AddGenericPictureAction("smartSoundMode", typeof(OffToOn), category: "sound", title: "Adaptive Sound Control"); + AddGenericPictureAction("soundOutput", typeof(SoundOutput), category: "sound", title: "Sound Output"); + AddGenericPictureAction("autoVolume", typeof(OffToOn), category: "sound", title: "Auto Volume"); + AddGenericPictureAction("virtualSurround", typeof(OffToOn), category: "sound", title: "Virtual Surround"); + + AddGenericPictureAction("screenShift", typeof(OffToOn), title: "Screen Shift"); + AddGenericPictureAction("logoLuminanceAdjust", typeof(LogoLuminance), title: "Logo Luminance"); + + //AddGenericPictureAction("enableToastPopup", typeof(OffToOn), category: "option", title: "enableToastPopup"); + AddSetConfigAction("tv.conti.supportUsedTime", typeof(BoolFalseToTrue), title: "Total Power On Time"); + + AddGenericPictureAction("wolwowlOnOff", typeof(FalseToTrue), category: "network", title: "Wake-On-LAN"); + AddLunaAction("TPC", typeof(BoolFalseToTrue), title: "Temporal Peak Luminance Control (TPC)", ModelYear.Series2020); + AddLunaAction("GSR", typeof(BoolFalseToTrue), title: "Global Sticky/Stress Reduction (GSR)", ModelYear.Series2020); + } + + public LgDevice(LgDeviceDto device) : this(device.Name, device.IpAddress, device.MacAddress, device.IsCustom, device.IsDummy) + { + UpdateOptions(device.Options); + } + + private void UpdateOptions(LgDeviceOptions options) + { + UseSecureConnection = options.UseSecureConnection; + TurnScreenOffOnScreenSaver = options.TurnScreenOffOnScreenSaver; + TurnScreenOnAfterScreenSaver = options.TurnScreenOnAfterScreenSaver; + ScreenSaverMinimalDuration = options.ScreenSaverMinimalDuration; + HandleManualScreenSaver = options.HandleManualScreenSaver; + PowerOffOnScreenSaver = options.PowerOffOnScreenSaver; + PowerOnAfterScreenSaver = options.PowerOnAfterScreenSaver; + PowerOnAfterManualPowerOff = options.PowerOnAfterManualPowerOff; + PowerOnAfterResume = options.PowerOnAfterResume; + PowerOffOnStandby = options.PowerOffOnStandby; + PowerOffOnShutdown = options.PowerOffOnShutdown; + PowerOffByWindows = options.PowerOffByWindows; + PowerOnByWindows = options.PowerOnByWindows; + PowerOnAfterStartup = options.PowerOnAfterStartup; + PowerOnAfterSessionUnlock = options.PowerOnAfterSessionUnlock; + PowerOffAfterSessionLock = options.PowerOffAfterSessionLock; + TriggersEnabled = options.TriggersEnabled; + HDMIPortNumber = options.HDMIPortNumber; + } + + ~LgDevice() + { + ClearPowerOffTask(); + } + + public bool Update(LgDeviceDto device) + { + var reconnect = false; + + Name = device.Name; + if (IpAddress != device.IpAddress) + { + IpAddress = device.IpAddress; + reconnect = true; + } + MacAddress = device.MacAddress; + IsCustom = device.IsCustom; + IsDummy = device.IsDummy; + + UpdateOptions(device.Options); + + return reconnect; + } + + private void AddInvokableAction(string name, Func, Task> function) + { + var action = new InvokableAction + { + Name = name, + AsyncFunction = function, + Category = "misc", + Title = name + }; + + _invokableActions.Add(action); + } + + private void AddInternalPresetAction(LgPreset preset) + { + var action = new InvokableAction + { + Name = preset.name, + Preset = preset, + Advanced = true + }; + + _invokableActions.Add(action); + } + + private void AddHdmiPictureAction(string name, Type type = null, decimal minValue = 0, decimal maxValue = 0, string category = "picture", string title = null, int numberOfValues = 1, ModelYear fromModelYear = ModelYear.None) + { + for (var i = 1; i <= 4; i++) + { + var replacedTitle = title?.Replace("HDMIX", $"HDMI{i}"); + + AddGenericPictureAction($"{name}{i}", type, minValue, maxValue, category, replacedTitle, numberOfValues, fromModelYear); + } + } + + private void AddGenericPictureAction(string name, Type type = null, decimal minValue = 0, decimal maxValue = 0, string category = "picture", string title = null, int numberOfValues = 1, ModelYear fromModelYear = ModelYear.None, List valueLabels = null) + { + var action = new InvokableAction + { + Name = name, + AsyncFunction = GenericPictureAction, + EnumType = type, + MinValue = minValue, + MaxValue = maxValue, + NumberOfValues = numberOfValues, + Category = category, + Title = title == null ? Utils.FirstCharUpperCase(name) : title, + FromModelYear = fromModelYear, + ValueLabels = valueLabels + }; + + _invokableActions.Add(action); + } + + private void AddSetDeviceConfigAction(string name, Type type, string title) + { + var action = new InvokableAction + { + Name = name, + AsyncFunction = GenericDeviceConfigAction, + EnumType = type, + Title = title == null ? Utils.FirstCharUpperCase(name) : title + }; + + _invokableActions.Add(action); + } + + private void AddSetConfigAction(string name, Type type, string title) + { + var action = new InvokableAction + { + Name = name, + AsyncFunction = GenericSetConfigAction, + EnumType = type, + Title = title == null ? Utils.FirstCharUpperCase(name) : title, + Category = "Config", + Advanced = true + }; + + _invokableActions.Add(action); + } + + private void AddLunaAction(string name, Type type, string title, ModelYear fromModelYear = ModelYear.None) + { + var action = new InvokableAction + { + Name = name, + AsyncFunction = GenericLunaAction, + EnumType = type, + Title = title == null ? Utils.FirstCharUpperCase(name) : title, + Category = "Config", + FromModelYear = fromModelYear, + Advanced = true + }; + + _invokableActions.Add(action); + } + + private void AddVolumeAction(string name, string title, ModelYear fromModelYear = ModelYear.None) + { + var action = new InvokableAction + { + Name = name, + AsyncFunction = GenericVolumeAction, + MinValue = 0, + MaxValue = 100, + Title = title == null ? Utils.FirstCharUpperCase(name) : title, + Category = "sound", + FromModelYear = fromModelYear + }; + + _invokableActions.Add(action); + } + + public void AddGameBarAction(string name) + { + if (!ActionsOnGameBar.Contains(name)) + { + ActionsOnGameBar.Add(name); + } + } + + public void RemoveGameBarAction(string name) + { + ActionsOnGameBar.Remove(name); + } + + public override string ToString() + { + //return (IsDummy ? string.Empty : (IsCustom ? "Custom: " : "Auto detect: ")) + $"{Name}" + (!string.IsNullOrEmpty(IpAddress) ? $" ({IpAddress})" : string.Empty); + return $"{(IsDummy ? string.Empty : (IsCustom ? "Custom: " : "Auto detect: "))}{Name}{(!string.IsNullOrEmpty(IpAddress) ? ", " + IpAddress : string.Empty)}"; + } + + public async Task Connect(int retries = 3) + { + var locked = _connectSemaphore.CurrentCount == 0; + await _connectSemaphore.WaitAsync(); + try + { + if (locked && _lgTvApi != null) + { + return true; + } + + try + { + await DisposeConnection(); + _lgTvApi = await LgTvApi.CreateLgTvApi(IpAddress, retries, UseSecureConnection); + + //Test(); + //_lgTvApi.Test3(); + if (_lgTvApi != null && !Utils.ConsoleOpened) + { + var info = await _lgTvApi.GetSystemInfo("modelName"); + if (info != null) + { + ModelName = info.modelName; + SetModelYear(); + } + + Logger.Debug($"LG TV with ip-address {IpAddress} has model name {ModelName}"); + + await _lgTvApi.SubscribeVolume(VolumeChanged); + await _lgTvApi.SubscribePowerState(PowerStateChanged); + await _lgTvApi.SubscribePictureSettings(PictureSettingsChanged); + await _lgTvApi.SubscribeForegroundApp(ForegroundAppChanged); + + //await _lgTvApi.SetConfig("tv.conti.supportUsedTime", true); + + //await _lgTvApi.Reboot(); + + //var result = await GetPictureSettings(); + + //await _lgTvApi.SetSystemSettings("adjustingLuminance", new[] { 0, 0, -5, -10, -15, -20, -25, -30, -35, -40, -45, -50, -50, -50, -40, -30, -20, -10, 10, 20, 30, 50 }); + //await _lgTvApi.SetSystemSettings("adjustingLuminance", new[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }); + + //await _lgTvApi.SetInput("HDMI_1"); + //await Task.Delay(2000); + //await _lgTvApi.SetConfig("com.palm.app.settings.enableHdmiPcLabel", true); + //await _lgTvApi.SetInput("HDMI_2"); + } + return _lgTvApi != null; + } + catch (Exception ex) + { + string logMessage = ex.ToLogString(Environment.StackTrace); + Logger.Error($"Error while connecting to {IpAddress}: {logMessage}"); + return false; + } + } + finally + { + _connectSemaphore.Release(); + } + } + + private void SetModelYear() + { + if (ModelName == null || ModelName.Contains("OLED") != true) + { + Year = ModelYear.None; + return; + } + + var normalized = ModelName.Replace("OLED", "").Replace(" ", ""); + + if (normalized.Length > 3) + { + normalized = normalized.Substring(3); + } + + var suffix = normalized.First(); + + Year = suffix switch + { + '6' => ModelYear.SeriesPre2018, + '7' => ModelYear.SeriesPre2018, + '8' => ModelYear.Series2018, + '9' => ModelYear.Series2019, + 'X' => ModelYear.Series2020, + '1' => ModelYear.Series2021, + '2' => ModelYear.Series2022, + '3' => ModelYear.Series2023, + _ => ModelYear.None + }; + + _invokableActions = _invokableActions.Where(a => Year == ModelYear.None || a.FromModelYear == ModelYear.None || Year >= a.FromModelYear).ToList(); + } + + public bool VolumeChanged(dynamic payload) + { + if (payload.volume != null) + { + PictureSettings.Volume = Utils.ParseDynamicAsInt(payload.volume, PictureSettings.Volume); + } + + var volumeAction = _invokableActions.FirstOrDefault(a => a.Name == "audioVolume"); + + if (volumeAction != null) + { + volumeAction.CurrentValue = PictureSettings.Volume; + } + + PictureSettingsChangedEvent?.Invoke(this, EventArgs.Empty); + + return true; + } + + public bool PictureSettingsChanged(dynamic payload) + { + if (payload.settings != null) + { + var settings = payload.settings; + + Logger.Debug($"[{Name}] PictureSettingsChanged: {JsonConvert.SerializeObject(settings)}"); + + if (settings.backlight != null) + { + PictureSettings.Backlight = Utils.ParseDynamicAsInt(settings.backlight, PictureSettings.Backlight); + } + if (settings.contrast != null) + { + PictureSettings.Contrast = Utils.ParseDynamicAsInt(settings.contrast, PictureSettings.Contrast); + } + if (settings.brightness != null) + { + PictureSettings.Brightness = Utils.ParseDynamicAsInt(settings.brightness, PictureSettings.Brightness); + } + if (settings.color != null) + { + PictureSettings.Color = Utils.ParseDynamicAsInt(settings.color, PictureSettings.Color); + } + } + + PictureSettingsChangedEvent?.Invoke(this, EventArgs.Empty); + + return true; + } + + public bool PowerStateChanged(dynamic payload) + { + Logger.Debug($"[{Name}] Power state change: {JsonConvert.SerializeObject(payload)}"); + + var state = payload.state != null ? ((string)payload.state).Replace(' ', '_') : PowerState.Unknown.ToString(); + + PowerState newState; + if (Enum.TryParse(state, out newState)) + { + CurrentState = newState; + + if (CurrentState == PowerState.Active) + { + if (payload.processing == null && (DateTimeOffset.Now - _poweredOffViaAppDateTime).TotalMilliseconds > 500) + { + PoweredOffViaApp = false; + PoweredOffBy = PowerOffSource.Unknown; + _poweredOffViaAppDateTime = DateTimeOffset.MinValue; + } + else if (payload.processing != null && ((string)payload.processing).Equals("Request Power Off", StringComparison.Ordinal)) + { + PoweredOffBy = PoweredOffViaApp ? PowerOffSource.App : PowerOffSource.Manually; + } + } + else if (CurrentState != PowerState.Screen_Off) + { + PoweredOffBy = PoweredOffViaApp ? PowerOffSource.App : PowerOffSource.Manually; + } + } + else + { + CurrentState = PowerState.Unknown; + Logger.Warn($"Unknown power state: {state}"); + } + + Logger.Debug($"PoweredOffBy: {PoweredOffBy}, PoweredOffViaApp: {PoweredOffViaApp}"); + + PowerStateChangedEvent?.Invoke(this, EventArgs.Empty); + + return true; + } + + public bool ForegroundAppChanged(dynamic payload) + { + Logger.Debug($"[{Name}] ForegroundAppChanged: {JsonConvert.SerializeObject(payload)}"); + + if (payload.appId != null) + { + CurrentAppId = payload.appId; + } + + return true; + } + + public bool IsConnected() + { + return !_lgTvApi?.ConnectionClosed ?? false; + } + + private async ValueTask DisposeConnection() + { + if (_lgTvApi != null) + { + await _lgTvApi.DisposeAsync(); + _lgTvApi = null; + } + } + + public async Task ExecutePreset(LgPreset preset, bool reconnect, LgServiceConfig config) + { + var hasApp = !string.IsNullOrEmpty(preset.appId); + + var hasWOL = preset.steps.Any(s => s.Equals("WOL", StringComparison.OrdinalIgnoreCase)); + + if (hasWOL) + { + var connected = await WakeAndConnect(0); + if (!connected) + { + return false; + } + } + + for (var tries = 0; tries <= 1; tries++) + { + if (!await Connected(reconnect || tries == 1)) + { + return false; + } + + if (hasApp) + { + try + { + var @params = preset.AppParams; + if (@params == null && config.ShowAdvancedActions) + { + if (preset.appId.Equals("com.webos.app.softwareupdate")) + { + @params = new { mode = "user", flagUpdate = true }; + } + else if (preset.appId.Equals("com.webos.app.factorywin")) + { + if (preset.name.Contains("ezadjust", StringComparison.OrdinalIgnoreCase)) + { + @params = new { id = "executeFactory", irKey = "ezAdjust" }; + } + else + { + @params = new { id = "executeFactory", irKey = "inStart" }; + } + } + } + + await _lgTvApi.LaunchApp(preset.appId, @params); + } + catch (Exception ex) + { + string logMessage = ex.ToLogString(Environment.StackTrace); + Logger.Error("Error while launching app: " + logMessage); + + if (tries == 0) + { + continue; + } + return false; + } + + if (_justWokeUp) + { + _justWokeUp = false; + await Task.Delay(1000); + } + } + + if (preset.steps.Any()) + { + if (hasApp) + { + await Task.Delay(1500); + } + try + { + await ExecuteSteps(_lgTvApi, preset, config); + } + catch (Exception ex) + { + string logMessage = ex.ToLogString(Environment.StackTrace); + Logger.Error("Error while executing steps: " + logMessage); + + if (tries == 0) + { + continue; + } + return false; + } + } + + return true; + } + + return true; + } + + private async Task ExecuteSteps(LgTvApi api, LgPreset preset, LgServiceConfig config) + { + LgWebOsMouseService mouse = null; + + foreach (var step in preset.steps) + { + var keySpec = step.Split(':'); + + var delay = 10; + var key = step; + if (keySpec.Length == 2) + { + delay = Utils.ParseInt(keySpec[1]); + if (delay > 0) + { + key = keySpec[0]; + } + } + + var index = key.IndexOf("("); + string[] parameters = null; + if (index > -1) + { + var keyValue = key.Split('('); + key = keyValue[0]; + parameters = keyValue[1].Substring(0, keyValue[1].Length - 1).Split(';'); + } + + var executeKey = true; + var action = _invokableActions.FirstOrDefault(a => a.Name.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (action != null) + { + await ExecuteActionAsync(action, parameters); + + executeKey = false; + } + if (parameters != null) + { + _serviceManager ??= Program.ServiceProvider.GetRequiredService(); + + if (await _serviceManager.HandleExternalServiceAsync(key, parameters)) + { + executeKey = false; + } + } + + if (executeKey) + { + mouse ??= await api.GetMouse(); + await SendKey(mouse, key); + delay = delay <= 10 ? config.DefaultButtonDelay : delay; + } + + if (delay > 0) + { + await Task.Delay(delay); + } + } + } + + public async Task ExecuteActionAsync(InvokableAction action, string[] parameters) + { + var function = action.AsyncFunction; + if (function == null) + { + return; + } + + if (parameters?.Length > 0) + { + var keyValues = new Dictionary { + { "name", action.Name }, + { "value", parameters }, + { "category", action.Category } + }; + + await function(keyValues); + return; + } + + await function(null); + } + + private async Task SendKey(LgWebOsMouseService mouse, string key) + { + key = key.ToUpper(); + if (key.Length >= 1 && int.TryParse(key[0].ToString(), out _)) + { + key = "_" + key; + } + var button = (ButtonType)Enum.Parse(typeof(ButtonType), key); + await mouse.SendButton(button); + } + + public async Task GetMouseAsync() + { + await CheckConnectionAsync(); + + return await _lgTvApi.GetMouse(); + } + + private async Task CheckConnectionAsync() + { + if (!await Connected()) + { + throw new InvalidOperationException("Not connected"); + } + } + + public async Task> GetApps(bool force = false) + { + if (!force) + { + await Task.Delay(5000); + } + + if (!await Connected(force)) + { + Logger.Debug("Cannot refresh apps: no connection could be made"); + return new List(); + } + + return await _lgTvApi.GetApps(force); + } + + public async Task PerformActionOnScreenSaver() + { + if (!IsPowerOffAllowed()) + { + return true; + } + + if (TurnScreenOffOnScreenSaver) + { + Logger.Debug("Turning screen off on screen saver"); + await _lgTvApi.TurnScreenOff(); + + return true; + } + + Logger.Debug($"Device {Name} is now powering off due to delayed screensaver task"); + + return await PowerOff(true, false); + } + + public async Task PerformActionAfterScreenSaver(int powerOnRetries) + { + if (TurnScreenOffOnScreenSaver && IsConnected()) + { + if (TurnScreenOnAfterScreenSaver) + { + try + { + Logger.Debug("Turning screen on after screen saver"); + await _lgTvApi.TurnScreenOn(); + } + catch (Exception) { } + } + + return true; + } + + return await WakeAndConnectWithRetries(powerOnRetries); + } + + internal async Task PowerOff(bool checkHdmi = false, bool reconnect = true) + { + if (!await Connected(reconnect) || CurrentState != PowerState.Active) + { + return false; + } + + if (checkHdmi && !IsPowerOffAllowed()) + { + return true; + } + + PoweredOffViaApp = true; + _poweredOffViaAppDateTime = DateTimeOffset.Now; + + try + { + await _lgTvApi.TurnOff().WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(true); + } + catch (TimeoutException ex) + { + Logger.Debug($"Timeout when turning off tv: {ex.Message}"); + } + + return true; + } + + public bool IsPowerOffAllowed() + { + if (CurrentAppId != null && HDMIPortNumber != 0 && !CurrentAppId.EndsWith($".hdmi{HDMIPortNumber}", StringComparison.InvariantCulture)) + { + Logger.Debug($"[{Name}] PowerOff/ScreenOff is ignored because current app {CurrentAppId} does not match configured HDMI port {HDMIPortNumber}"); + return false; + } + + return true; + } + + internal async Task TestConnection(int retries = 1) + { + if (!await Connected(true, retries)) + { + return false; + } + + try + { + await _lgTvApi.IsMuted(); + return true; + } + catch (Exception ex) + { + Logger.Error("TestConnection: " + ex.ToLogString()); + return false; + } + } + + internal async Task WakeAndConnect(int wakeDelay = 5000, int connectDelay = 500) + { + try + { + await Task.Delay(wakeDelay); + var result = Wake(); + if (!result) + { + Logger.Debug("WOL failed"); + return false; + } + Logger.Debug("WOL succeeded"); + await Task.Delay(connectDelay); + result = await Connect(1); + Logger.Debug("Connect succeeded: " + result); + return result; + } + catch (Exception e) + { + Logger.Error("WakeAndConnectToSelectedDevice: " + e.ToLogString()); + return false; + } + } + + internal bool Wake() + { + var result = false; + + if (MacAddress != null) + { + var wolService = Program.ServiceProvider.GetRequiredService(); + + result = wolService.SendWol(MacAddress, IpAddress); + _justWokeUp = true; + } + else + { + Logger.Debug("Cannot wake device: the device has no MAC-address"); + } + + return result; + } + + internal async Task WakeAndConnectWithRetries(int retries = 5) + { + var wakeDelay = 0; + var maxRetries = retries <= 1 ? 5 : retries; + + var result = false; + for (var retry = 0; retry < maxRetries && !result; retry++) + { + Logger.Debug($"WakeAndConnectWithRetries: attempt {retry + 1} of {maxRetries}..."); + + var ms = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + result = await WakeAndConnect(retry == 0 ? wakeDelay : 0); + ms = DateTimeOffset.Now.ToUnixTimeMilliseconds() - ms; + if (!result) + { + var delay = 2000 - ms; + if (delay > 0) + { + await Task.Delay((int)delay); + } + } + } + + return result; + } + + public List GetInvokableActions(bool includedAdvanced = false) + { + return _invokableActions.Where(a => includedAdvanced || !a.Advanced).ToList(); + } + + public List GetInvokableActionsForGameBar() + { + var actions = GetInvokableActions(); + return actions.Where(a => + a.Category == "picture" && (a.EnumType != null && a.EnumType != typeof(PictureMode) || a.MinValue >= 0 && a.MaxValue > 0) || + a.Name == "audioVolume" || (a.Category == "other" && (a.MinValue < a.MaxValue || a.EnumType != null)) + ) + .ToList(); + } + + public List GetActionsForGameBar() + { + var actions = GetInvokableActions(); + return actions.Where(a => ActionsOnGameBar.Contains(a.Name)).ToList(); + } + + public void Test() + { + try + { + //_lgTvApi.GetServiceList(); + //_lgTvApi?.Test(); + } + catch (Exception ex) + { + Logger.Error("TEST: " + ex.ToLogString()); + } + } + + private async Task Connected(bool reconnect = false, int retries = 3) + { + if (reconnect || !IsConnected() || !string.Equals(_lgTvApi.GetIpAddress(), IpAddress)) + { + if (!await Connect(retries)) + { + Logger.Debug("Cannot apply LG-preset: no connection could be made"); + return false; + } + } + return true; + } + + internal void ConvertToCustom() + { + IsCustom = true; + } + + public bool IsUsingHDRPictureMode() + { + // Temporary workaround because I cannot read the picture mode at this time + return PictureSettings.Contrast == 100; + } + + internal async Task SetBacklight(int backlight) + { + await CheckConnectionAsync(); + + await _lgTvApi.SetSystemSettings("backlight", backlight.ToString()); + } + + internal async Task SetContrast(int contrast) + { + await CheckConnectionAsync(); + + await _lgTvApi.SetSystemSettings("contrast", contrast.ToString()); + } + + public async Task SetOLEDMotionPro(string mode) + { + await CheckConnectionAsync(); + + await _lgTvApi.SetConfig("tv.model.motionProMode", mode); + } + + public async Task SetSvcMenuFlag(bool enabled) + { + await CheckConnectionAsync(); + + await _lgTvApi.SetSystemSettings("svcMenuFlag", enabled, "other"); + } + + internal async Task SetConfig(string key, object value) + { + await CheckConnectionAsync(); + + await _lgTvApi.SetConfig(key, value); + } + internal async Task SetSystemSettings(string name, string value) + { + await CheckConnectionAsync(); + + await _lgTvApi.SetSystemSettings(name, value); + + UpdateCurrentValueOfAction(name, value); + } + + internal async Task GetPictureSettings() + { + //var keys = new[] { "backlight", "brightness", "contrast", "color", "pictureMode", "colorGamut", "dynamicContrast", "peakBrightness", "smoothGradation", "energySaving", "motionProOLED" }; + //var keys = new[] { "backlight", "brightness", "contrast", "color" }; + + await CheckConnectionAsync(); + + return await _lgTvApi.GetSystemSettings2("picture"); + } - private Task WakeAction(Dictionary _) - { - return Task.FromResult(Wake()); - } + private Task WakeAction(Dictionary _) + { + return Task.FromResult(Wake()); + } - private async Task TurnScreenOffAction(Dictionary _) - { - await CheckConnectionAsync(); + private async Task TurnScreenOffAction(Dictionary _) + { + await CheckConnectionAsync(); - await _lgTvApi.TurnScreenOff(); + await _lgTvApi.TurnScreenOff(); - return true; - } + return true; + } - private async Task TurnScreenOnAction(Dictionary _) - { - await CheckConnectionAsync(); + private async Task TurnScreenOnAction(Dictionary _) + { + await CheckConnectionAsync(); - await _lgTvApi.TurnScreenOn(); + await _lgTvApi.TurnScreenOn(); - return true; - } + return true; + } - private async Task RebootAction(Dictionary _) - { - await CheckConnectionAsync(); + private async Task RebootAction(Dictionary _) + { + await CheckConnectionAsync(); - await _lgTvApi.Reboot(); + await _lgTvApi.Reboot(); - return true; - } + return true; + } - private async Task GenericPictureAction(Dictionary parameters) - { - await CheckConnectionAsync(); + private async Task GenericPictureAction(Dictionary parameters) + { + await CheckConnectionAsync(); - var settingName = parameters["name"].ToString(); - var stringValues = parameters["value"] as string[]; - var category = parameters["category"].ToString(); - object value = stringValues[0]; - if (stringValues.Length > 1) - { - value = stringValues.Select(s => int.Parse(s)).ToArray(); - } - await _lgTvApi.SetSystemSettings(settingName, value, category); + var settingName = parameters["name"].ToString(); + var stringValues = parameters["value"] as string[]; + var category = parameters["category"].ToString(); + object value = stringValues[0]; + if (stringValues.Length > 1) + { + value = stringValues.Select(s => int.Parse(s)).ToArray(); + } + await _lgTvApi.SetSystemSettings(settingName, value, category); - UpdateCurrentValueOfAction(settingName, value.ToString()); + UpdateCurrentValueOfAction(settingName, value.ToString()); - return true; - } + return true; + } - private async Task GenericDeviceConfigAction(Dictionary parameters) - { - await CheckConnectionAsync(); + private async Task GenericDeviceConfigAction(Dictionary parameters) + { + await CheckConnectionAsync(); - var id = parameters["name"].ToString().Replace("_icon", string.Empty); - var stringValues = parameters["value"] as string[]; - var value = stringValues[0]; + var id = parameters["name"].ToString().Replace("_icon", string.Empty); + var stringValues = parameters["value"] as string[]; + var value = stringValues[0]; - var description = Utils.GetDescriptionByEnumName(value); + var description = Utils.GetDescriptionByEnumName(value); - await _lgTvApi.SetDeviceConfig(id, value, description); + await _lgTvApi.SetDeviceConfig(id, value, description); - return true; - } + return true; + } - private async Task GenericSetConfigAction(Dictionary parameters) - { - await CheckConnectionAsync(); + private async Task GenericSetConfigAction(Dictionary parameters) + { + await CheckConnectionAsync(); - var key = parameters["name"].ToString(); - var values = parameters["value"] as object[]; - var value = values[0]; + var key = parameters["name"].ToString(); + var values = parameters["value"] as object[]; + var value = values[0]; - await _lgTvApi.SetConfig(key, value); + await _lgTvApi.SetConfig(key, value); - return true; - } + return true; + } - private async Task GenericLunaAction(Dictionary parameters) - { - await CheckConnectionAsync(); + private async Task GenericLunaAction(Dictionary parameters) + { + await CheckConnectionAsync(); - var key = parameters["name"].ToString(); - var values = parameters["value"] as object[]; - var value = values[0]; + var key = parameters["name"].ToString(); + var values = parameters["value"] as object[]; + var value = values[0]; - await _lgTvApi.SetTpcOrGsr(key, value?.ToString() == "bool_true"); + await _lgTvApi.SetTpcOrGsr(key, value?.ToString() == "bool_true"); - return true; - } + return true; + } - private async Task GenericVolumeAction(Dictionary parameters) - { - await CheckConnectionAsync(); + private async Task GenericVolumeAction(Dictionary parameters) + { + await CheckConnectionAsync(); - var values = parameters["value"] as object[]; - var value = values[0]; + var values = parameters["value"] as object[]; + var value = values[0]; - var volume = Utils.ParseInt(value.ToString(), 0); + var volume = Utils.ParseInt(value.ToString(), 0); - await _lgTvApi.SetVolume(volume); + await _lgTvApi.SetVolume(volume); - return true; - } + return true; + } - public Task SetVolume(int value) - { - return _lgTvApi.SetVolume(value); - } + public Task SetVolume(int value) + { + return _lgTvApi.SetVolume(value); + } - private void UpdateCurrentValueOfAction(string settingName, string value) - { - if (settingName == "backlight" || settingName == "contrast" || settingName == "brightness" || settingName == "color") - { - return; - } - var action = _invokableActions.FirstOrDefault(a => a.Name == settingName); - if (action != null) - { - if (action.EnumType != null) - { - try - { - var enumValue = Enum.Parse(action.EnumType, value); - var intEnum = (int)enumValue; - action.CurrentValue = intEnum; - } - catch (Exception) { } - } - else - { - action.CurrentValue = 0; - } - } - } - - internal void ClearPowerOffTask() - { - _powerOffTimer?.Dispose(); - } - - internal void PowerOffIn(int seconds) - { - ClearPowerOffTask(); + private void UpdateCurrentValueOfAction(string settingName, string value) + { + if (settingName == "backlight" || settingName == "contrast" || settingName == "brightness" || settingName == "color") + { + return; + } + var action = _invokableActions.FirstOrDefault(a => a.Name == settingName); + if (action != null) + { + if (action.EnumType != null) + { + try + { + var enumValue = Enum.Parse(action.EnumType, value); + var intEnum = (int)enumValue; + action.CurrentValue = intEnum; + } + catch (Exception) { } + } + else + { + action.CurrentValue = 0; + } + } + } + + internal void ClearPowerOffTask() + { + _powerOffTimer?.Dispose(); + } + + internal void PowerOffIn(int seconds) + { + ClearPowerOffTask(); - _powerOffTimer = new Timer(PowerOffByTimer); - _powerOffTimer.Change(TimeSpan.FromSeconds(seconds), TimeSpan.FromSeconds(seconds)); - } - - private void PowerOffByTimer(object _) - { - ClearPowerOffTask(); - - var x = PerformActionOnScreenSaver(); - } - - internal async Task ExecuteInvokableAction(InvokableActionDto invokableAction, List parameters) - { - var action = _invokableActions.FirstOrDefault(a => a.Name == invokableAction.Name); - - if (action == null) - { - return false; - } - - await ExecuteActionAsync(action, parameters?.ToArray() ?? []); - - return true; - } - } + _powerOffTimer = new Timer(PowerOffByTimer); + _powerOffTimer.Change(TimeSpan.FromSeconds(seconds), TimeSpan.FromSeconds(seconds)); + } + + private void PowerOffByTimer(object _) + { + ClearPowerOffTask(); + + var x = PerformActionOnScreenSaver(); + } + + internal async Task ExecuteInvokableAction(InvokableActionDto invokableAction, List parameters) + { + var action = _invokableActions.FirstOrDefault(a => a.Name == invokableAction.Name); + + if (action == null) + { + return false; + } + + await ExecuteActionAsync(action, parameters?.ToArray() ?? []); + + return true; + } + } } diff --git a/ColorControl/Services/LG/LgService.cs b/ColorControl/Services/LG/LgService.cs index 364997d..147872a 100644 --- a/ColorControl/Services/LG/LgService.cs +++ b/ColorControl/Services/LG/LgService.cs @@ -22,1255 +22,1254 @@ using System.Windows.Forms; namespace ColorControl.Services.LG { - class LgService : ServiceBase - { - protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - - public static string LgListAppsJson = "listApps.json"; - - public override string ServiceName => "LG"; - - public List Devices { get; private set; } - public LgDevice SelectedDevice - { - get { return _selectedDevice; } - set - { - _selectedDevice = value; - Config.PreferredMacAddress = _selectedDevice != null ? _selectedDevice.MacAddress : null; - - SelectedDeviceChangedEvent?.Invoke(_selectedDevice, EventArgs.Empty); - } - } - public event EventHandler SelectedDeviceChangedEvent; - - public LgServiceConfig Config { get; private set; } - - protected override string PresetsBaseFilename => "LgPresets.json"; - - private string _configFilename; - private bool _allowPowerOn; - private readonly PowerEventDispatcher _powerEventDispatcher; - private readonly SessionSwitchDispatcher _sessionSwitchDispatcher; - private readonly ProcessEventDispatcher _processEventDispatcher; - private readonly DisplayChangeEventDispatcher _displayChangeEventDispatcher; - private readonly ServiceManager _serviceManager; - private LgDevice _selectedDevice; - private string _rcButtonsFilename; - private List _remoteControlButtons; - private List _lgApps = new List(); - - private bool _poweredOffByScreenSaver; - private int _poweredOffByScreenSaverProcessId; - private RestartDetector _restartDetector; - private readonly WinApiService _winApiService; - private readonly WinApiAdminService _winApiAdminService; - private SynchronizationContext _syncContext; - private LgGameBar _gameBarForm; - - public ProcessChangedEventArgs CurrentProcessEvent => _processEventDispatcher?.MonitorContext; - - public static readonly int SHORTCUTID_LGQA = -202; - public static readonly int SHORTCUTID_GAMEBAR = -101; - - public LgService( - GlobalContext globalContext, - PowerEventDispatcher powerEventDispatcher, - SessionSwitchDispatcher sessionSwitchDispatcher, - ProcessEventDispatcher processEventDispatcher, - DisplayChangeEventDispatcher displayChangeEventDispatcher, - ServiceManager serviceManager, - RestartDetector restartDetector, - WinApiService winApiService, - WinApiAdminService winApiAdminService) : base(globalContext) - { - _allowPowerOn = globalContext.StartUpParams.RunningFromScheduledTask; - _powerEventDispatcher = powerEventDispatcher; - _sessionSwitchDispatcher = sessionSwitchDispatcher; - _processEventDispatcher = processEventDispatcher; - _displayChangeEventDispatcher = displayChangeEventDispatcher; - _serviceManager = serviceManager; - LgPreset.GetDeviceName += LgPreset_GetDeviceName; - LgPreset.GetAppName += LgPreset_GetAppName; - - _restartDetector = restartDetector; - _winApiService = winApiService; - _winApiAdminService = winApiAdminService; - _syncContext = globalContext.SynchronizationContext; - LgTvApiCore.SyncContext = _syncContext; - - LoadConfig(); - LoadPresets(); - LoadRemoteControlButtons(); - } - - private string LgPreset_GetAppName(string appId) - { - var name = appId; - - var item = _lgApps.FirstOrDefault(x => x.appId.Equals(appId)); - if (item != null) - { - name = item.title + " (" + appId + ")"; - } - - return name; - } - - private string LgPreset_GetDeviceName(string macAddress) - { - var device = Devices?.FirstOrDefault(d => !string.IsNullOrEmpty(d.MacAddress) && d.MacAddress.Equals(macAddress)); - if (device != null) - { - return device.Name; - } - - return "Unknown: device not found"; - } - - ~LgService() - { - GlobalSave(); - } - - protected override async Task HotKeyPressed(object sender, KeyboardShortcutEventArgs e, CancellationToken _) - { - await base.HotKeyPressed(sender, e, _); - - if (e.HotKeyId == SHORTCUTID_GAMEBAR) - { - ToggleGameBar(); - return; - } - } - - public override List GetInfo() - { - return [$"{_presets.Count} presets", $"{Devices?.Count ?? 0} devices"]; - } - - public static async Task ExecutePresetAsync(string presetName) - { - try - { - var lgService = Program.ServiceProvider.GetRequiredService(); - - await lgService.RefreshDevices(afterStartUp: true); - - var result = await lgService.ApplyPreset(presetName); - - if (!result) - { - Console.WriteLine("Preset not found or error while executing."); - } - - return result; - } - catch (Exception ex) - { - Console.WriteLine("Error executing preset: " + ex.ToLogString()); - return false; - } - } - - public void GlobalSave() - { - SaveConfig(); - SavePresets(); - SaveRemoteControlButtons(); - - SendConfigToService(); - } - - protected override List GetDefaultPresets() - { - List presets = null; - - var defaultPresetsFileName = Path.Combine(Directory.GetCurrentDirectory(), "LgPresets.json"); - if (File.Exists(defaultPresetsFileName)) - { - var json = File.ReadAllText(defaultPresetsFileName); - - presets = JsonConvert.DeserializeObject>(json); - } - - return presets; - } - - private void LoadConfig() - { - _configFilename = Path.Combine(_dataPath, "LgConfig.json"); - try - { - if (File.Exists(_configFilename)) - { - Config = JsonConvert.DeserializeObject(File.ReadAllText(_configFilename)); - } - } - catch (Exception ex) - { - Logger.Error($"LoadConfig: {ex.Message}"); - } - Config ??= new LgServiceConfig(); - } - - private void LoadRemoteControlButtons() - { - var defaultButtons = GenerateDefaultRemoteControlButtons(); - - _rcButtonsFilename = Path.Combine(_dataPath, "LgRemoteControlButtons.json"); - //if (File.Exists(_rcButtonsFilename)) - //{ - // var json = File.ReadAllText(_rcButtonsFilename); - - // _remoteControlButtons = _JsonDeserializer.Deserialize>(json); - - // var missingButtons = defaultButtons.Where(b => !_remoteControlButtons.Any(x => x.name.Equals(b.name))); - // _remoteControlButtons.AddRange(missingButtons); - //} - //else - { - _remoteControlButtons = defaultButtons; - } - } - - public List GetRemoteControlButtons() - { - return _remoteControlButtons; - } - - private List GenerateDefaultRemoteControlButtons() - { - var list = new List(); - - for (var i = 1; i < 11; i++) - { - var button = i < 10 ? i : 0; - list.Add(GeneratePreset(button.ToString())); - } - - list.Add(GeneratePreset("Power", step: "POWER")); - list.Add(GeneratePreset("Wol", step: "WOL")); - list.Add(GeneratePreset("Home", appId: "com.webos.app.home", key: Keys.H)); - list.Add(GeneratePreset("Settings", appId: "com.palm.app.settings", key: Keys.S)); - list.Add(GeneratePreset("TV Guide", appId: "com.webos.app.livemenu")); - list.Add(GeneratePreset("List", step: "LIST")); - list.Add(GeneratePreset("SAP", step: "SAP")); - - list.Add(GeneratePreset("Vol +", step: "VOLUMEUP", key: Keys.VolumeUp)); - list.Add(GeneratePreset("Vol -", step: "VOLUMEDOWN", key: Keys.VolumeDown)); - list.Add(GeneratePreset("Mute", step: "MUTE", key: Keys.VolumeMute)); - list.Add(GeneratePreset("Channel +", step: "CHANNELUP", key: Keys.MediaNextTrack)); - list.Add(GeneratePreset("Channel -", step: "CHANNELDOWN", key: Keys.MediaPreviousTrack)); - - list.Add(GeneratePreset("Up", step: "UP", key: Keys.Up)); - list.Add(GeneratePreset("Down", step: "DOWN", key: Keys.Down)); - list.Add(GeneratePreset("Left", step: "LEFT", key: Keys.Left)); - list.Add(GeneratePreset("Right", step: "RIGHT", key: Keys.Right)); - - list.Add(GeneratePreset("Enter", step: "ENTER", key: Keys.Enter)); - list.Add(GeneratePreset("Back", step: "BACK", key: Keys.Back)); - list.Add(GeneratePreset("Exit", step: "EXIT", key: Keys.Escape)); - - list.Add(GeneratePreset("Netflix", appId: "netflix")); - list.Add(GeneratePreset("Inputs", appId: "com.webos.app.homeconnect")); - list.Add(GeneratePreset("Amazon Prime", appId: "amazon")); - - list.Add(GeneratePreset("Red", step: "RED")); - list.Add(GeneratePreset("Green", step: "GREEN")); - list.Add(GeneratePreset("Yellow", step: "YELLOW")); - list.Add(GeneratePreset("Blue", step: "BLUE")); - - list.Add(GeneratePreset("Rakuten TV", appId: "ui30")); - list.Add(GeneratePreset("Play", step: "PLAY")); - list.Add(GeneratePreset("Pause", step: "PAUSE")); - - return list; - } - - private LgPreset GeneratePreset(string name, string step = null, string appId = null, Keys key = Keys.None) - { - var preset = new LgPreset(); - - var cvt = new KeysConverter(); - var shortcut = key != Keys.None ? (string)cvt.ConvertTo(key, typeof(string)) : null; - - preset.name = name; - preset.appId = appId; - preset.shortcut = shortcut; - if (appId == null) - { - preset.steps.Add(step == null ? name : step); - } - - return preset; - } - - private void SaveConfig() - { - Config.PowerOnAfterStartup = Devices?.Any(d => d.PowerOnAfterStartup) ?? false; - - Utils.WriteObject(_configFilename, Config); - } - - private void SaveRemoteControlButtons() - { - Utils.WriteObject(_rcButtonsFilename, _remoteControlButtons); - } - - public async Task RefreshDevices(bool connect = true, bool afterStartUp = false) - { - Devices = Config.Devices; - - if (!_globalContext.StartUpParams.NoDeviceRefresh) - { - var customIpAddresses = Devices.Where(d => d.IsCustom).Select(d => d.IpAddress); - - var pnpDevices = await Utils.GetPnpDevices(Config.DeviceSearchKey); - - var autoDevices = pnpDevices.Where(p => !customIpAddresses.Contains(p.IpAddress)).Select(d => new LgDevice(d.Name, d.IpAddress, d.MacAddress, false)).ToList(); - var autoIpAddresses = pnpDevices.Select(d => d.IpAddress); - - Devices.RemoveAll(d => !d.IsCustom && !autoIpAddresses.Contains(d.IpAddress)); - - var newAutoDevices = autoDevices.Where(ad => ad.IpAddress != null && !Devices.Any(d => d.IpAddress != null && d.IpAddress.Equals(ad.IpAddress))); - Devices.AddRange(newAutoDevices); - } - - if (Devices.Any()) - { - var preferredDevice = Devices.FirstOrDefault(x => x.MacAddress != null && x.MacAddress.Equals(Config.PreferredMacAddress)) ?? Devices[0]; - - SelectedDevice = preferredDevice; - } - else - { - SelectedDevice = null; - } - - if (afterStartUp && SelectedDevice == null && Config.PowerOnAfterStartup && !string.IsNullOrEmpty(Config.PreferredMacAddress)) - { - Logger.Debug("No device has been found, trying to wake it first..."); - - var tempDevice = new LgDevice("Test", string.Empty, Config.PreferredMacAddress); - tempDevice.Wake(); - - await Task.Delay(4000); - return await RefreshDevices(); - } - - foreach (var device in Devices) - { - device.PowerStateChangedEvent += LgDevice_PowerStateChangedEvent; - device.PictureSettingsChangedEvent += Device_PictureSettingsChangedEvent; - } - - if (connect && SelectedDevice != null) - { - if (_allowPowerOn) - { - await WakeAfterStartupOrResume(); - } - else - { - if (afterStartUp) - { - var _ = SelectedDevice.Connect(); - } - else - { - return await SelectedDevice.Connect(1); - } - } - } - - return true; - } - - private void Device_PictureSettingsChangedEvent(object sender, EventArgs e) - { - // - } - - public override async Task ApplyPreset(LgPreset preset) - { - return await ApplyPreset(preset, false); - } - - public async Task ApplyPreset(LgPreset preset, bool reconnect = false) - { - var device = GetPresetDevice(preset); - - if (device == null) - { - Logger.Debug("Cannot apply preset: no device has been selected"); - return false; - } - - var result = await device.ExecutePreset(preset, reconnect, Config); - - _lastAppliedPreset = preset; - - return result; - } - - public LgDevice GetPresetDevice(LgPreset preset) - { - var device = string.IsNullOrEmpty(preset.DeviceMacAddress) - ? SelectedDevice - : Devices.FirstOrDefault(d => d.MacAddress?.Equals(preset.DeviceMacAddress, StringComparison.OrdinalIgnoreCase) ?? false); - return device; - } - - public async Task GetMouseAsync() - { - return await SelectedDevice?.GetMouseAsync(); - } - - public async Task RefreshApps(bool force = false) - { - if (SelectedDevice == null) - { - Logger.Debug("Cannot refresh apps: no device has been selected"); - return; - } - - var apps = await SelectedDevice.GetApps(force); - - if (apps.Any()) - { - _lgApps.Clear(); - _lgApps.AddRange(apps); - } - } - - public List GetApps() - { - return _lgApps; - } - - internal async Task PowerOff() - { - return await SelectedDevice?.PowerOff(); - } - - internal bool WakeSelectedDevice() - { - return SelectedDevice?.Wake() ?? false; - } - - internal Task WakeAfterStartupOrResume(PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) - { - Devices.ForEach(d => d.ClearPowerOffTask()); - - var wakeDevices = Devices.Where(d => state == PowerOnOffState.StartUp && d.PowerOnAfterStartup || - state == PowerOnOffState.Resume && d.PowerOnAfterResume || - state == PowerOnOffState.ScreenSaver && d.PowerOnAfterScreenSaver); - - return PowerOnDevicesTask(wakeDevices, state, checkUserSession); - } - - public override void InstallEventHandlers() - { - base.InstallEventHandlers(); - - SetShortcuts(SHORTCUTID_LGQA, Config.QuickAccessShortcut); - - var keyboardShortcutDispatcher = _globalContext.ServiceProvider.GetService(); - keyboardShortcutDispatcher?.RegisterShortcut(SHORTCUTID_GAMEBAR, Config.GameBarShortcut); - - _powerEventDispatcher.RegisterAsyncEventHandler(PowerEventDispatcher.Event_Startup, PowerStateChanged); - _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_Suspend, PowerModeChanged); - _powerEventDispatcher.RegisterAsyncEventHandler(PowerEventDispatcher.Event_Resume, PowerModeResume); - _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_Shutdown, PowerModeChanged); - _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_MonitorOff, PowerModeChanged); - _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_MonitorOn, PowerModeChanged); - - _sessionSwitchDispatcher.RegisterEventHandler(SessionSwitchDispatcher.Event_SessionSwitch, SessionSwitched); - _sessionSwitchDispatcher.RegisterAsyncEventHandler(SessionSwitchDispatcher.Event_SessionSwitch, SessionSwitchedAsync); - - _processEventDispatcher.RegisterAsyncEventHandler(ProcessEventDispatcher.Event_ProcessChanged, ProcessChanged); - - _displayChangeEventDispatcher.RegisterAsyncEventHandler(DisplayChangeEventDispatcher.Event_DisplayChanged, DisplayChanged); - } - - private async Task DisplayChanged(object sender, DisplayChangedEventArgs e, CancellationToken ct) - { - await ExecutePresetsForEvent(PresetTriggerType.DisplayChange); - } - - private async Task PowerStateChanged(object sender, PowerStateChangedEventArgs e, CancellationToken token) - { - await RefreshDevices(afterStartUp: true); - - if (_globalContext.StartUpParams.RunningFromScheduledTask) - { - await ExecutePresetsForEvent(PresetTriggerType.Startup); - } - - - if (_globalContext.StartUpParams.ExecuteLgPreset) - { - await ExecutePresetAsync(_globalContext.StartUpParams.LgPresetName); - } - } - - private void SessionSwitched(object sender, SessionSwitchEventArgs e) - { - if (!_sessionSwitchDispatcher.UserLocalSession) - { - return; - } - - if (_poweredOffByScreenSaver) - { - Logger.Debug("User switched to local and screen was powered off due to screensaver: waking up"); - _poweredOffByScreenSaver = false; - _poweredOffByScreenSaverProcessId = 0; - WakeAfterStartupOrResume(); - } - else - { - Logger.Debug("User switched to local but screen was not powered off by screensaver: NOT waking up"); - } - } - - private async Task SessionSwitchedAsync(object sender, SessionSwitchEventArgs e, CancellationToken token) - { - if (e.Reason == SessionSwitchReason.SessionLock) - { - var devices = Devices.Where(d => d.PowerOffAfterSessionLock); - PowerOffDevices(devices, PowerOnOffState.StandBy); - } - else if (e.Reason == SessionSwitchReason.SessionUnlock) - { - var devices = Devices.Where(d => d.PowerOnAfterSessionUnlock).ToList(); - await PowerOnDevicesTask(devices); - } - } - - private async Task PowerModeResume(object sender, PowerStateChangedEventArgs e, CancellationToken _) - { - Logger.Debug($"PowerModeChanged: {e.State}"); - - await WakeAfterStartupOrResume(PowerOnOffState.Resume); - - await ExecutePresetsForEvent(PresetTriggerType.Resume); - } - - private void PowerModeChanged(object sender, PowerStateChangedEventArgs e) - { - Logger.Debug($"PowerModeChanged: {e.State}"); - - if (Devices?.Any() != true) - { - Logger.Debug("Devices have not been loaded, ignoring event"); - return; - } - - switch (e.State) - { - case PowerOnOffState.StandBy: - { - _ = ExecutePresetsForEvent(PresetTriggerType.Standby); - - var devices = Devices.Where(d => d.PowerOffOnStandby); - PowerOffDevices(devices, PowerOnOffState.StandBy); - break; - } - case PowerOnOffState.ShutDown: - { - _ = ExecutePresetsForEvent(PresetTriggerType.Shutdown); - - var devices = Devices.Where(d => d.PowerOffOnShutdown); - - PowerOffDevices(devices); - break; - } - - case PowerOnOffState.MonitorOff: - { - var devices = Devices.Where(d => d.PowerOffByWindows).ToList(); - PowerOffDevices(devices, PowerOnOffState.MonitorOff); - break; - } - case PowerOnOffState.MonitorOn: - { - var devices = Devices.Where(d => d.PowerOnByWindows).ToList(); - PowerOnDevicesTask(devices); - break; - } - } - } - - private async Task ExecutePresetsForEvent(PresetTriggerType triggerType) - { - var presets = _presets.Where(p => p.Triggers.Any(t => t.Trigger == triggerType)).ToList(); - - if (!presets.Any()) - { - return; - } - - var applicableDevices = Devices.Where(d => d.TriggersEnabled && - presets.Any(p => (string.IsNullOrEmpty(p.DeviceMacAddress) && d == SelectedDevice) || p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase))).ToList(); - - if (!applicableDevices.Any()) - { - return; - } - - await ExecuteEventPresets(_serviceManager, new[] { triggerType }, isHDRActive: applicableDevices.Any(d => d.IsUsingHDRPictureMode())).ConfigureAwait(true); - } - - private void SessionEnded(object sender, SessionEndedEventArgs e) - { - //if (e.Reason == SessionEndReasons.SystemShutdown) - //{ - // Logger.Debug($"SessionEnded: {e.Reason}"); - - // if (Config.PowerOffOnShutdown) - // { - // PowerOff(); - // } - //} - } - - private void LgDevice_PowerStateChangedEvent(object sender, EventArgs e) - { - if (!(sender is LgDevice device)) - { - return; - } - - if (device.CurrentState == LgDevice.PowerState.Active) - { - if (Config.SetSelectedDeviceByPowerOn) - { - SelectedDevice = device; - } - } - } - - public async Task ProcessChanged(object sender, ProcessChangedEventArgs args, CancellationToken token) - { - try - { - var wasConnected = Devices?.Any(d => (d.PowerOffOnScreenSaver || d.PowerOnAfterScreenSaver) && d.IsConnected()); - - if (!wasConnected.HasValue) - { - return; - } - - var applicableDevices = Devices.Where(d => - d.PowerOffOnScreenSaver - || d.PowerOnAfterScreenSaver - || (d.TriggersEnabled - // only these trigger types are ever triggered by a process changed event - && _presets.Any(p => p.Triggers.Any(t => t.Trigger == PresetTriggerType.ProcessSwitch || t.Trigger == PresetTriggerType.ScreensaverStart || t.Trigger == PresetTriggerType.ScreensaverStop) - && ((string.IsNullOrEmpty(p.DeviceMacAddress) && d == SelectedDevice) || p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase))))) - .ToList(); - - if (!applicableDevices.Any()) - { - return; - } - - if (_poweredOffByScreenSaver && !_sessionSwitchDispatcher.UserLocalSession) - { - return; - } - - var processes = args.RunningProcesses; - - if (_poweredOffByScreenSaver) - { - if (processes.Any(p => p.Id == _poweredOffByScreenSaverProcessId)) - { - return; - } - - var process = processes.FirstOrDefault(p => p.ProcessName.ToLowerInvariant().EndsWith(".scr")); - - if (process != null) - { - Logger.Debug($"Detected screen saver process switch from {_poweredOffByScreenSaverProcessId} to {process.Id}"); - - _poweredOffByScreenSaverProcessId = process.Id; - return; - } - - Logger.Debug("Screensaver stopped, waking"); - _poweredOffByScreenSaver = false; - _poweredOffByScreenSaverProcessId = 0; - - await WakeAfterStartupOrResume(PowerOnOffState.ScreenSaver); - - await ExecuteScreenSaverPresetsCustom(args, applicableDevices); - - return; - } - - var connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); - - if (!connectedDevices.Any()) - { - if (wasConnected == true) - { - Logger.Debug("Process monitor: TV(s) where connected, but not any longer"); - wasConnected = false; - - return; - } - else - { - var devices = applicableDevices.ToList(); - foreach (var device in devices) - { - Logger.Debug($"Process monitor: test connection with {device.Name}..."); - - var test = await device.TestConnection(); - Logger.Debug("Process monitor: test connection result: " + test); - } - - connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); - - if (!connectedDevices.Any()) - { - return; - } - } - } - - if (wasConnected == false) - { - Logger.Debug("Process monitor: TV(s) where not connected, but connection has now been established"); - wasConnected = true; - } - - if (connectedDevices.Any(d => d.PowerOffOnScreenSaver || d.PowerOnAfterScreenSaver)) - { - await HandleScreenSaverProcessAsync(processes, connectedDevices); - } - - await ExecuteProcessPresets(args, connectedDevices); - - await ExecuteScreenSaverPresetsCustom(args, connectedDevices); - } - catch (Exception ex) - { - Logger.Error("ProcessChanged: " + ex.ToLogString()); - } - } - - private async Task ExecuteScreenSaverPresetsCustom(ProcessChangedEventArgs context, IList connectedDevices) - { - var triggerDevices = connectedDevices.Where(d => d.TriggersEnabled).ToList(); - - if (!triggerDevices.Any()) - { - return; - } - - await ExecuteScreenSaverPresets(_serviceManager, context, triggerDevices.Any(d => d.IsUsingHDRPictureMode())); - } - - private async Task ExecuteProcessPresets(ProcessChangedEventArgs context, IList connectedDevices) - { - var triggerDevices = connectedDevices.Where(d => d.TriggersEnabled).ToList(); - - if (context.IsScreenSaverActive) - { - return; - } - - var selectedDevice = SelectedDevice; - - var presets = _presets.Where(p => p.Triggers.Any(t => t.Trigger == PresetTriggerType.ProcessSwitch) && - ((string.IsNullOrEmpty(p.DeviceMacAddress) && selectedDevice?.TriggersEnabled == true) - || Devices.Any(d => d.TriggersEnabled && p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase)))); - if (!presets.Any()) - { - return; - } - - if (context.ForegroundProcess != null && context.ForegroundProcessIsFullScreen) - { - presets = presets.Where(p => p.Triggers.Any(t => t.Conditions.HasFlag(PresetConditionType.FullScreen))); - } - else if (context.StoppedFullScreen) - { - presets = presets.Where(p => p.Triggers.Any(t => !t.Conditions.HasFlag(PresetConditionType.FullScreen))); - } - - var triggerContext = await CreateTriggerContext(_serviceManager, context, triggerDevices.Any(d => d.IsUsingHDRPictureMode())); - - var toApplyPresets = presets.Where(p => p.Triggers.Any(t => t.TriggerActive(triggerContext))).ToList(); - - if (toApplyPresets.Any()) - { - var toApplyPreset = toApplyPresets.FirstOrDefault(p => toApplyPresets.Count == 1 || p.Triggers.Any(t => !t.IncludedProcesses.Contains("*"))) ?? toApplyPresets.First(); - - if (_lastTriggeredPreset != toApplyPreset) - { - Logger.Debug($"Applying triggered preset: {toApplyPreset.name} because trigger conditions are met"); - - await ApplyPreset(toApplyPreset); - - _lastTriggeredPreset = toApplyPreset; - } - } - } - - private async Task HandleScreenSaverProcessAsync(IList processes, List connectedDevices) - { - var process = processes.FirstOrDefault(p => p.ProcessName.ToLowerInvariant().EndsWith(".scr")); - - var powerOffDevices = connectedDevices.Where(d => d.PowerOffOnScreenSaver).ToList(); - - if (process == null) - { - powerOffDevices.ForEach(d => d.ClearPowerOffTask()); - - return; - } - - var parent = process.Parent(processes); - - // Ignore manually started screen savers if no devices prefer this - if (parent?.ProcessName.Contains("winlogon") != true && !connectedDevices.Any(d => d.HandleManualScreenSaver)) - { - return; - } - - Logger.Debug($"Screensaver started: {process.ProcessName}, parent: {parent.ProcessName}"); - try - { - foreach (var device in powerOffDevices) - { - Logger.Debug($"Screensaver check: test connection with {device.Name}..."); - - var test = await device.TestConnection(); - Logger.Debug("Screensaver check: test connection result: " + test); - if (!test) - { - continue; - } - - if (device.ScreenSaverMinimalDuration > 0) - { - Logger.Debug($"Screensaver check: powering off tv {device.Name} in {device.ScreenSaverMinimalDuration} seconds because of screensaver"); - - _poweredOffByScreenSaver = true; - - device.PowerOffIn(device.ScreenSaverMinimalDuration); - - continue; - } - - Logger.Debug($"Screensaver check: powering off tv {device.Name} because of screensaver"); - try - { - _poweredOffByScreenSaver = await device.PerformActionOnScreenSaver().WaitAsync(TimeSpan.FromSeconds(5)); - } - catch (TimeoutException) - { - // Assume powering off was successful - _poweredOffByScreenSaver = true; - Logger.Error($"Screensaver check: timeout exception while powering off (probably connection closed)"); - } - catch (Exception pex) - { - // Assume powering off was successful - _poweredOffByScreenSaver = true; - Logger.Error($"Screensaver check: exception while powering off (probably connection closed): " + pex.Message); - } - Logger.Debug($"Screensaver check: powered off tv {device.Name}"); - } - - _poweredOffByScreenSaverProcessId = process.Id; - } - catch (Exception e) - { - Logger.Error($"Screensaver check: can't power off: " + e.ToLogString()); - } - } - - internal void AddCustomDevice(LgDevice device) - { - Devices.Add(device); - } - - internal void RemoveCustomDevice(LgDevice device) - { - Devices.Remove(device); - - if (!string.IsNullOrEmpty(device.MacAddress)) - { - var presets = _presets.Where(p => !string.IsNullOrEmpty(p.DeviceMacAddress) && p.DeviceMacAddress.Equals(device.MacAddress, StringComparison.OrdinalIgnoreCase)).ToList(); - presets.ForEach(preset => - { - preset.DeviceMacAddress = null; - }); - } - - if (SelectedDevice == device) - { - SelectedDevice = Devices.Any() ? Devices.First() : null; - } - } - - public void PowerOffDevices(IEnumerable devices, PowerOnOffState state = PowerOnOffState.ShutDown) - { - if (!devices.Any()) - { - return; - } - - if (NativeMethods.GetAsyncKeyState(NativeConstants.VK_CONTROL) < 0 || NativeMethods.GetAsyncKeyState(NativeConstants.VK_RCONTROL) < 0) - { - Logger.Debug("Not powering off because CONTROL-key is down"); - return; - } - - if (state == PowerOnOffState.ShutDown) - { - var sleep = Config.ShutdownDelay; - - Logger.Debug($"PowerOffDevices: Waiting for a maximum of {sleep} milliseconds..."); - - while (sleep > 0 && _restartDetector?.PowerOffDetected == false) - { - Thread.Sleep(100); - - if (_restartDetector != null && (_restartDetector.RestartDetected || _restartDetector.IsRebootInProgress())) - { - Logger.Debug("Not powering off because of a restart"); - return; - } - - sleep -= 100; - } - } - - Logger.Debug("Powering off tv(s)..."); - var tasks = new List(); - foreach (var device in devices) - { - var task = device.PowerOff(true, false); - - tasks.Add(task); - } - - if (state == PowerOnOffState.StandBy) - { - var standByScript = Path.Combine(Program.DataDir, "StandByScript.bat"); - if (File.Exists(standByScript)) - { - _winApiAdminService.StartProcess(standByScript, hidden: true, wait: true); - } - } - - // We can't use async here because we need to stay on the main thread... - var _ = Task.WhenAll(tasks.ToArray()).ConfigureAwait(true); - - Logger.Debug("Done powering off tv(s)"); - } - - public Task PowerOnDevicesTask(IEnumerable devices, PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) - { - return Task.Run(async () => await PowerOnDevices(devices, state, checkUserSession)); - } - - public async Task PowerOnDevices(IEnumerable devices, PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) - { - if (!devices.Any()) - { - return; - } - - if (checkUserSession && !_sessionSwitchDispatcher.UserLocalSession) - { - Logger.Debug($"WakeAfterStartupOrResume: not waking because session info indicates no local session"); - return; - } - - var resumeScript = Path.Combine(Program.DataDir, "ResumeScript.bat"); - - var tasks = new List(); - - foreach (var device in devices) - { - if (!device.PowerOnAfterManualPowerOff && device.PoweredOffBy == LgDevice.PowerOffSource.Manually) - { - Logger.Debug($"[{device.Name}]: device was manually powered off by user, not powering on"); - continue; - } - - if (state == PowerOnOffState.ScreenSaver) - { - tasks.Add(device.PerformActionAfterScreenSaver(Config.PowerOnRetries)); - continue; - } - - var task = device.WakeAndConnectWithRetries(Config.PowerOnRetries); - tasks.Add(task); - } - - await Task.WhenAll(tasks.ToArray()); - - if (File.Exists(resumeScript)) - { - _winApiAdminService.StartProcess(resumeScript, hidden: true); - } - } - - private void SendConfigToService() - { - if (!_winApiService.IsServiceRunning()) - { - return; - } - - var json = JsonConvert.SerializeObject(Config); - - var message = new SvcMessage - { - MessageType = SvcMessageType.SetLgConfig, - Data = json - }; - - PipeUtils.SendMessage(message); - } - - public async Task ExecuteEventPresets(PresetTriggerType triggerType) - { - await ExecuteEventPresets(_serviceManager, new[] { triggerType }); - } - - public void ToggleGameBar() - { - if (_gameBarForm == null || !_gameBarForm.Visible) - { - if (_gameBarForm == null || _gameBarForm.IsDisposed) - { - if (SelectedDevice == null) - { - return; - } - - _gameBarForm = new LgGameBar(this); - } - - _gameBarForm.Show(); - _gameBarForm.Activate(); - } - else - { - _gameBarForm?.Hide(); - } - } - - public List GetDevices() - { - if (Devices == null) - { - return new List(); - } - - return Devices.Select(d => new LgDeviceDto - { - Name = d.Name, - MacAddress = d.MacAddress, - IpAddress = d.IpAddress, - IsCustom = d.IsCustom, - IsDummy = d.IsDummy, - Options = new LgDeviceOptions - { - HandleManualScreenSaver = d.HandleManualScreenSaver, - HDMIPortNumber = d.HDMIPortNumber, - PowerOffByWindows = d.PowerOffByWindows, - PowerOffOnScreenSaver = d.PowerOffOnScreenSaver, - PowerOffOnShutdown = d.PowerOffOnShutdown, - PowerOffOnStandby = d.PowerOffOnStandby, - PowerOnAfterManualPowerOff = d.PowerOnAfterManualPowerOff, - PowerOnAfterResume = d.PowerOnAfterResume, - PowerOnAfterScreenSaver = d.PowerOnAfterScreenSaver, - PowerOnAfterStartup = d.PowerOnAfterStartup, - PowerOnByWindows = d.PowerOnByWindows, - PowerOffAfterSessionLock = d.PowerOffAfterSessionLock, - PowerOnAfterSessionUnlock = d.PowerOnAfterSessionUnlock, - ScreenSaverMinimalDuration = d.ScreenSaverMinimalDuration, - TriggersEnabled = d.TriggersEnabled, - TurnScreenOffOnScreenSaver = d.TurnScreenOffOnScreenSaver, - TurnScreenOnAfterScreenSaver = d.TurnScreenOnAfterScreenSaver, - UseSecureConnection = d.UseSecureConnection - }, - IsConnected = d.IsConnected(), - IsSelected = d == SelectedDevice - }).ToList(); - } - - public async Task UpdateDevice(LgDeviceDto deviceSpec) - { - if (Devices == null) - { - return false; - } - - var device = Devices.FirstOrDefault(d => d.MacAddress == deviceSpec.MacAddress); - - if (device == null) - { - device = new LgDevice(deviceSpec); - - Devices.Add(device); - } - else - { - var reconnect = device.Update(deviceSpec); - - if (reconnect) - { - await device.Connect(); - } - } - - SaveConfig(); - - return true; - } - - public async Task TestDevice(LgDeviceDto deviceSpec) - { - if (Devices == null) - { - return false; - } - - try - { - var device = new LgDevice(deviceSpec); - - return await device.TestConnection(); - } - catch - { - return false; - } - } - - public LgServiceConfigDto GetConfig() - { - return new LgServiceConfigDto - { - DefaultButtonDelay = Config.DefaultButtonDelay, - DeviceSearchKey = Config.DeviceSearchKey, - //Devices = GetDevices(), - PowerOnAfterStartup = Config.PowerOnAfterStartup, - PowerOnRetries = Config.PowerOnRetries, - PreferredMacAddress = Config.PreferredMacAddress, - QuickAccessShortcut = Config.QuickAccessShortcut, - SetSelectedDeviceByPowerOn = Config.SetSelectedDeviceByPowerOn, - ShowAdvancedActions = Config.ShowAdvancedActions, - ShutdownDelay = Config.ShutdownDelay, - GameBarShortcut = Config.GameBarShortcut, - GameBarShowingTime = Config.GameBarShowingTime - }; - } - - public bool UpdateConfig(LgServiceConfigDto config) - { - Config.Update(config); - - SetShortcuts(SHORTCUTID_LGQA, Config.QuickAccessShortcut); - - SaveConfig(); - - return true; - } - - public bool UpdatePreset(LgPreset specPreset) - { - var currentPreset = _presets.FirstOrDefault(p => p.id == specPreset.id); - - if (currentPreset != null) - { - currentPreset.Update(specPreset); - - SavePresets(); - return true; - } - - var newPreset = new LgPreset(specPreset); - newPreset.name = specPreset.name; - - _presets.Add(newPreset); - - SavePresets(); - - return true; - } - - public List> GetInvokableActions() - { - return SelectedDevice?.GetInvokableActions(Config.ShowAdvancedActions).Select(a => new InvokableActionDto - { - Advanced = a.Advanced, - Category = Utils.FirstCharUpperCase(a.Category ?? "Misc"), - CurrentValue = a.CurrentValue, - EnumType = a.EnumType, - MaxValue = a.MaxValue, - MinValue = a.MinValue, - Name = a.Name, - NumberOfValues = a.NumberOfValues, - Preset = a.Preset, - Title = a.Title ?? Utils.FirstCharUpperCase(a.Name), - ValueLabels = a.ValueLabels - }).ToList(); - } - - public async Task ExecuteInvokableAction(InvokableActionDto invokableAction, List parameters = null) - { - var device = SelectedDevice; - - if (invokableAction.SelectedDevice != null) - { - device = MatchDevice(invokableAction.SelectedDevice) ?? SelectedDevice; - } - - if (invokableAction.Preset != null) - { - await SelectedDevice?.ExecutePreset(invokableAction.Preset, false, Config); - - return true; - } - - return await device?.ExecuteInvokableAction(invokableAction, parameters); - } - - public bool SetSelectedDevice(LgDeviceDto deviceDto) - { - if (deviceDto == null) - { - return false; - } - - var device = MatchDevice(deviceDto); - if (device == null) - { - return false; - } - - SelectedDevice = device; - - return true; - } - - private LgDevice MatchDevice(LgDeviceDto deviceDto) - { - return Devices?.FirstOrDefault(d => string.IsNullOrEmpty(deviceDto.MacAddress) ? d.IpAddress == deviceDto.IpAddress : d.MacAddress == deviceDto.MacAddress); - } - } + class LgService : ServiceBase + { + protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + public static string LgListAppsJson = "listApps.json"; + + public override string ServiceName => "LG"; + + public List Devices { get; private set; } + public LgDevice SelectedDevice + { + get { return _selectedDevice; } + set + { + _selectedDevice = value; + Config.PreferredMacAddress = _selectedDevice != null ? _selectedDevice.MacAddress : null; + + SelectedDeviceChangedEvent?.Invoke(_selectedDevice, EventArgs.Empty); + } + } + public event EventHandler SelectedDeviceChangedEvent; + + public LgServiceConfig Config { get; private set; } + + protected override string PresetsBaseFilename => "LgPresets.json"; + + private string _configFilename; + private bool _allowPowerOn; + private readonly PowerEventDispatcher _powerEventDispatcher; + private readonly SessionSwitchDispatcher _sessionSwitchDispatcher; + private readonly ProcessEventDispatcher _processEventDispatcher; + private readonly DisplayChangeEventDispatcher _displayChangeEventDispatcher; + private readonly ServiceManager _serviceManager; + private LgDevice _selectedDevice; + private string _rcButtonsFilename; + private List _remoteControlButtons; + private List _lgApps = new List(); + + private bool _poweredOffByScreenSaver; + private int _poweredOffByScreenSaverProcessId; + private RestartDetector _restartDetector; + private readonly WinApiService _winApiService; + private readonly WinApiAdminService _winApiAdminService; + private LgGameBar _gameBarForm; + + public ProcessChangedEventArgs CurrentProcessEvent => _processEventDispatcher?.MonitorContext; + + public static readonly int SHORTCUTID_LGQA = -202; + public static readonly int SHORTCUTID_GAMEBAR = -101; + + public LgService( + GlobalContext globalContext, + PowerEventDispatcher powerEventDispatcher, + SessionSwitchDispatcher sessionSwitchDispatcher, + ProcessEventDispatcher processEventDispatcher, + DisplayChangeEventDispatcher displayChangeEventDispatcher, + ServiceManager serviceManager, + RestartDetector restartDetector, + WinApiService winApiService, + WinApiAdminService winApiAdminService) : base(globalContext) + { + _allowPowerOn = globalContext.StartUpParams.RunningFromScheduledTask; + _powerEventDispatcher = powerEventDispatcher; + _sessionSwitchDispatcher = sessionSwitchDispatcher; + _processEventDispatcher = processEventDispatcher; + _displayChangeEventDispatcher = displayChangeEventDispatcher; + _serviceManager = serviceManager; + LgPreset.GetDeviceName += LgPreset_GetDeviceName; + LgPreset.GetAppName += LgPreset_GetAppName; + + _restartDetector = restartDetector; + _winApiService = winApiService; + _winApiAdminService = winApiAdminService; + + LoadConfig(); + LoadPresets(); + LoadRemoteControlButtons(); + } + + private string LgPreset_GetAppName(string appId) + { + var name = appId; + + var item = _lgApps.FirstOrDefault(x => x.appId.Equals(appId)); + if (item != null) + { + name = item.title + " (" + appId + ")"; + } + + return name; + } + + private string LgPreset_GetDeviceName(string macAddress) + { + var device = Devices?.FirstOrDefault(d => !string.IsNullOrEmpty(d.MacAddress) && d.MacAddress.Equals(macAddress)); + if (device != null) + { + return device.Name; + } + + return "Unknown: device not found"; + } + + ~LgService() + { + GlobalSave(); + } + + protected override async Task HotKeyPressed(object sender, KeyboardShortcutEventArgs e, CancellationToken _) + { + await base.HotKeyPressed(sender, e, _); + + if (e.HotKeyId == SHORTCUTID_GAMEBAR) + { + ToggleGameBar(); + return; + } + } + + public override List GetInfo() + { + return [$"{_presets.Count} presets", $"{Devices?.Count ?? 0} devices"]; + } + + public static async Task ExecutePresetAsync(string presetName) + { + try + { + var lgService = Program.ServiceProvider.GetRequiredService(); + + await lgService.RefreshDevices(afterStartUp: true); + + var result = await lgService.ApplyPreset(presetName); + + if (!result) + { + Console.WriteLine("Preset not found or error while executing."); + } + + return result; + } + catch (Exception ex) + { + Console.WriteLine("Error executing preset: " + ex.ToLogString()); + return false; + } + } + + public void GlobalSave() + { + SaveConfig(); + SavePresets(); + SaveRemoteControlButtons(); + + SendConfigToService(); + } + + protected override List GetDefaultPresets() + { + List presets = null; + + var defaultPresetsFileName = Path.Combine(Directory.GetCurrentDirectory(), "LgPresets.json"); + if (File.Exists(defaultPresetsFileName)) + { + var json = File.ReadAllText(defaultPresetsFileName); + + presets = JsonConvert.DeserializeObject>(json); + } + + return presets; + } + + private void LoadConfig() + { + _configFilename = Path.Combine(_dataPath, "LgConfig.json"); + try + { + if (File.Exists(_configFilename)) + { + Config = JsonConvert.DeserializeObject(File.ReadAllText(_configFilename)); + } + } + catch (Exception ex) + { + Logger.Error($"LoadConfig: {ex.Message}"); + } + Config ??= new LgServiceConfig(); + } + + private void LoadRemoteControlButtons() + { + var defaultButtons = GenerateDefaultRemoteControlButtons(); + + _rcButtonsFilename = Path.Combine(_dataPath, "LgRemoteControlButtons.json"); + //if (File.Exists(_rcButtonsFilename)) + //{ + // var json = File.ReadAllText(_rcButtonsFilename); + + // _remoteControlButtons = _JsonDeserializer.Deserialize>(json); + + // var missingButtons = defaultButtons.Where(b => !_remoteControlButtons.Any(x => x.name.Equals(b.name))); + // _remoteControlButtons.AddRange(missingButtons); + //} + //else + { + _remoteControlButtons = defaultButtons; + } + } + + public List GetRemoteControlButtons() + { + return _remoteControlButtons; + } + + private List GenerateDefaultRemoteControlButtons() + { + var list = new List(); + + for (var i = 1; i < 11; i++) + { + var button = i < 10 ? i : 0; + list.Add(GeneratePreset(button.ToString())); + } + + list.Add(GeneratePreset("Power", step: "POWER")); + list.Add(GeneratePreset("Wol", step: "WOL")); + list.Add(GeneratePreset("Home", appId: "com.webos.app.home", key: Keys.H)); + list.Add(GeneratePreset("Settings", appId: "com.palm.app.settings", key: Keys.S)); + list.Add(GeneratePreset("TV Guide", appId: "com.webos.app.livemenu")); + list.Add(GeneratePreset("List", step: "LIST")); + list.Add(GeneratePreset("SAP", step: "SAP")); + + list.Add(GeneratePreset("Vol +", step: "VOLUMEUP", key: Keys.VolumeUp)); + list.Add(GeneratePreset("Vol -", step: "VOLUMEDOWN", key: Keys.VolumeDown)); + list.Add(GeneratePreset("Mute", step: "MUTE", key: Keys.VolumeMute)); + list.Add(GeneratePreset("Channel +", step: "CHANNELUP", key: Keys.MediaNextTrack)); + list.Add(GeneratePreset("Channel -", step: "CHANNELDOWN", key: Keys.MediaPreviousTrack)); + + list.Add(GeneratePreset("Up", step: "UP", key: Keys.Up)); + list.Add(GeneratePreset("Down", step: "DOWN", key: Keys.Down)); + list.Add(GeneratePreset("Left", step: "LEFT", key: Keys.Left)); + list.Add(GeneratePreset("Right", step: "RIGHT", key: Keys.Right)); + + list.Add(GeneratePreset("Enter", step: "ENTER", key: Keys.Enter)); + list.Add(GeneratePreset("Back", step: "BACK", key: Keys.Back)); + list.Add(GeneratePreset("Exit", step: "EXIT", key: Keys.Escape)); + + list.Add(GeneratePreset("Netflix", appId: "netflix")); + list.Add(GeneratePreset("Inputs", appId: "com.webos.app.homeconnect")); + list.Add(GeneratePreset("Amazon Prime", appId: "amazon")); + + list.Add(GeneratePreset("Red", step: "RED")); + list.Add(GeneratePreset("Green", step: "GREEN")); + list.Add(GeneratePreset("Yellow", step: "YELLOW")); + list.Add(GeneratePreset("Blue", step: "BLUE")); + + list.Add(GeneratePreset("Rakuten TV", appId: "ui30")); + list.Add(GeneratePreset("Play", step: "PLAY")); + list.Add(GeneratePreset("Pause", step: "PAUSE")); + + return list; + } + + private LgPreset GeneratePreset(string name, string step = null, string appId = null, Keys key = Keys.None) + { + var preset = new LgPreset(); + + var cvt = new KeysConverter(); + var shortcut = key != Keys.None ? (string)cvt.ConvertTo(key, typeof(string)) : null; + + preset.name = name; + preset.appId = appId; + preset.shortcut = shortcut; + if (appId == null) + { + preset.steps.Add(step == null ? name : step); + } + + return preset; + } + + private void SaveConfig() + { + Config.PowerOnAfterStartup = Devices?.Any(d => d.PowerOnAfterStartup) ?? false; + + Utils.WriteObject(_configFilename, Config); + } + + private void SaveRemoteControlButtons() + { + Utils.WriteObject(_rcButtonsFilename, _remoteControlButtons); + } + + public async Task RefreshDevices(bool connect = true, bool afterStartUp = false) + { + Devices = Config.Devices; + + if (!_globalContext.StartUpParams.NoDeviceRefresh) + { + var customIpAddresses = Devices.Where(d => d.IsCustom).Select(d => d.IpAddress); + + var pnpDevices = await Utils.GetPnpDevices(Config.DeviceSearchKey); + + var autoDevices = pnpDevices.Where(p => !customIpAddresses.Contains(p.IpAddress)).Select(d => new LgDevice(d.Name, d.IpAddress, d.MacAddress, false)).ToList(); + var autoIpAddresses = pnpDevices.Select(d => d.IpAddress); + + Devices.RemoveAll(d => !d.IsCustom && !autoIpAddresses.Contains(d.IpAddress)); + + var newAutoDevices = autoDevices.Where(ad => ad.IpAddress != null && !Devices.Any(d => d.IpAddress != null && d.IpAddress.Equals(ad.IpAddress))); + Devices.AddRange(newAutoDevices); + } + + if (Devices.Any()) + { + var preferredDevice = Devices.FirstOrDefault(x => x.MacAddress != null && x.MacAddress.Equals(Config.PreferredMacAddress)) ?? Devices[0]; + + SelectedDevice = preferredDevice; + } + else + { + SelectedDevice = null; + } + + if (afterStartUp && SelectedDevice == null && Config.PowerOnAfterStartup && !string.IsNullOrEmpty(Config.PreferredMacAddress)) + { + Logger.Debug("No device has been found, trying to wake it first..."); + + var tempDevice = new LgDevice("Test", string.Empty, Config.PreferredMacAddress); + tempDevice.Wake(); + + await Task.Delay(4000); + return await RefreshDevices(); + } + + foreach (var device in Devices) + { + device.PowerStateChangedEvent += LgDevice_PowerStateChangedEvent; + device.PictureSettingsChangedEvent += Device_PictureSettingsChangedEvent; + } + + if (connect && SelectedDevice != null) + { + if (_allowPowerOn) + { + await WakeAfterStartupOrResume(); + } + else + { + if (afterStartUp) + { + var _ = SelectedDevice.Connect(); + } + else + { + return await SelectedDevice.Connect(1); + } + } + } + + return true; + } + + private void Device_PictureSettingsChangedEvent(object sender, EventArgs e) + { + // + } + + public override async Task ApplyPreset(LgPreset preset) + { + return await ApplyPreset(preset, false); + } + + public async Task ApplyPreset(LgPreset preset, bool reconnect = false) + { + var device = GetPresetDevice(preset); + + if (device == null) + { + Logger.Debug("Cannot apply preset: no device has been selected"); + return false; + } + + var result = await device.ExecutePreset(preset, reconnect, Config); + + _lastAppliedPreset = preset; + + return result; + } + + public LgDevice GetPresetDevice(LgPreset preset) + { + var device = string.IsNullOrEmpty(preset.DeviceMacAddress) + ? SelectedDevice + : Devices.FirstOrDefault(d => d.MacAddress?.Equals(preset.DeviceMacAddress, StringComparison.OrdinalIgnoreCase) ?? false); + return device; + } + + public async Task GetMouseAsync() + { + return await SelectedDevice?.GetMouseAsync(); + } + + public async Task RefreshApps(bool force = false) + { + if (SelectedDevice == null) + { + Logger.Debug("Cannot refresh apps: no device has been selected"); + return; + } + + var apps = await SelectedDevice.GetApps(force); + + if (apps.Any()) + { + _lgApps.Clear(); + _lgApps.AddRange(apps); + } + } + + public List GetApps() + { + return _lgApps; + } + + internal async Task PowerOff() + { + return await SelectedDevice?.PowerOff(); + } + + internal bool WakeSelectedDevice() + { + return SelectedDevice?.Wake() ?? false; + } + + internal Task WakeAfterStartupOrResume(PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) + { + Devices.ForEach(d => d.ClearPowerOffTask()); + + var wakeDevices = Devices.Where(d => state == PowerOnOffState.StartUp && d.PowerOnAfterStartup || + state == PowerOnOffState.Resume && d.PowerOnAfterResume || + state == PowerOnOffState.ScreenSaver && d.PowerOnAfterScreenSaver); + + return PowerOnDevicesTask(wakeDevices, state, checkUserSession); + } + + public override void InstallEventHandlers() + { + base.InstallEventHandlers(); + + SetShortcuts(SHORTCUTID_LGQA, Config.QuickAccessShortcut); + + var keyboardShortcutDispatcher = _globalContext.ServiceProvider.GetService(); + keyboardShortcutDispatcher?.RegisterShortcut(SHORTCUTID_GAMEBAR, Config.GameBarShortcut); + + _powerEventDispatcher.RegisterAsyncEventHandler(PowerEventDispatcher.Event_Startup, PowerStateChanged); + _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_Suspend, PowerModeChanged); + _powerEventDispatcher.RegisterAsyncEventHandler(PowerEventDispatcher.Event_Resume, PowerModeResume); + _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_Shutdown, PowerModeChanged); + _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_MonitorOff, PowerModeChanged); + _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_MonitorOn, PowerModeChanged); + + _sessionSwitchDispatcher.RegisterEventHandler(SessionSwitchDispatcher.Event_SessionSwitch, SessionSwitched); + _sessionSwitchDispatcher.RegisterAsyncEventHandler(SessionSwitchDispatcher.Event_SessionSwitch, SessionSwitchedAsync); + + _processEventDispatcher.RegisterAsyncEventHandler(ProcessEventDispatcher.Event_ProcessChanged, ProcessChanged); + + _displayChangeEventDispatcher.RegisterAsyncEventHandler(DisplayChangeEventDispatcher.Event_DisplayChanged, DisplayChanged); + } + + private async Task DisplayChanged(object sender, DisplayChangedEventArgs e, CancellationToken ct) + { + await ExecutePresetsForEvent(PresetTriggerType.DisplayChange); + } + + private async Task PowerStateChanged(object sender, PowerStateChangedEventArgs e, CancellationToken token) + { + await RefreshDevices(afterStartUp: true); + + if (_globalContext.StartUpParams.RunningFromScheduledTask) + { + await ExecutePresetsForEvent(PresetTriggerType.Startup); + } + + + if (_globalContext.StartUpParams.ExecuteLgPreset) + { + await ExecutePresetAsync(_globalContext.StartUpParams.LgPresetName); + } + } + + private void SessionSwitched(object sender, SessionSwitchEventArgs e) + { + if (!_sessionSwitchDispatcher.UserLocalSession) + { + return; + } + + if (_poweredOffByScreenSaver) + { + Logger.Debug("User switched to local and screen was powered off due to screensaver: waking up"); + _poweredOffByScreenSaver = false; + _poweredOffByScreenSaverProcessId = 0; + WakeAfterStartupOrResume(); + } + else + { + Logger.Debug("User switched to local but screen was not powered off by screensaver: NOT waking up"); + } + } + + private async Task SessionSwitchedAsync(object sender, SessionSwitchEventArgs e, CancellationToken token) + { + if (e.Reason == SessionSwitchReason.SessionLock) + { + var devices = Devices.Where(d => d.PowerOffAfterSessionLock); + PowerOffDevices(devices, PowerOnOffState.StandBy); + } + else if (e.Reason == SessionSwitchReason.SessionUnlock) + { + var devices = Devices.Where(d => d.PowerOnAfterSessionUnlock).ToList(); + await PowerOnDevicesTask(devices); + } + } + + private async Task PowerModeResume(object sender, PowerStateChangedEventArgs e, CancellationToken _) + { + Logger.Debug($"PowerModeChanged: {e.State}"); + + await WakeAfterStartupOrResume(PowerOnOffState.Resume); + + await ExecutePresetsForEvent(PresetTriggerType.Resume); + } + + private void PowerModeChanged(object sender, PowerStateChangedEventArgs e) + { + Logger.Debug($"PowerModeChanged: {e.State}"); + + if (Devices?.Any() != true) + { + Logger.Debug("Devices have not been loaded, ignoring event"); + return; + } + + switch (e.State) + { + case PowerOnOffState.StandBy: + { + _ = ExecutePresetsForEvent(PresetTriggerType.Standby); + + var devices = Devices.Where(d => d.PowerOffOnStandby); + PowerOffDevices(devices, PowerOnOffState.StandBy); + break; + } + case PowerOnOffState.ShutDown: + { + _ = ExecutePresetsForEvent(PresetTriggerType.Shutdown); + + var devices = Devices.Where(d => d.PowerOffOnShutdown); + + PowerOffDevices(devices); + break; + } + + case PowerOnOffState.MonitorOff: + { + var devices = Devices.Where(d => d.PowerOffByWindows).ToList(); + PowerOffDevices(devices, PowerOnOffState.MonitorOff); + break; + } + case PowerOnOffState.MonitorOn: + { + var devices = Devices.Where(d => d.PowerOnByWindows).ToList(); + PowerOnDevicesTask(devices); + break; + } + } + } + + private async Task ExecutePresetsForEvent(PresetTriggerType triggerType) + { + var presets = _presets.Where(p => p.Triggers.Any(t => t.Trigger == triggerType)).ToList(); + + if (!presets.Any()) + { + return; + } + + var applicableDevices = Devices.Where(d => d.TriggersEnabled && + presets.Any(p => (string.IsNullOrEmpty(p.DeviceMacAddress) && d == SelectedDevice) || p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase))).ToList(); + + if (!applicableDevices.Any()) + { + return; + } + + await ExecuteEventPresets(_serviceManager, new[] { triggerType }, isHDRActive: applicableDevices.Any(d => d.IsUsingHDRPictureMode())).ConfigureAwait(true); + } + + private void SessionEnded(object sender, SessionEndedEventArgs e) + { + //if (e.Reason == SessionEndReasons.SystemShutdown) + //{ + // Logger.Debug($"SessionEnded: {e.Reason}"); + + // if (Config.PowerOffOnShutdown) + // { + // PowerOff(); + // } + //} + } + + private void LgDevice_PowerStateChangedEvent(object sender, EventArgs e) + { + if (!(sender is LgDevice device)) + { + return; + } + + if (device.CurrentState == LgDevice.PowerState.Active) + { + if (Config.SetSelectedDeviceByPowerOn) + { + SelectedDevice = device; + } + } + } + + public async Task ProcessChanged(object sender, ProcessChangedEventArgs args, CancellationToken token) + { + try + { + var wasConnected = Devices?.Any(d => (d.PowerOffOnScreenSaver || d.PowerOnAfterScreenSaver) && d.IsConnected()); + + if (!wasConnected.HasValue) + { + return; + } + + var applicableDevices = Devices.Where(d => + d.PowerOffOnScreenSaver + || d.PowerOnAfterScreenSaver + || (d.TriggersEnabled + // only these trigger types are ever triggered by a process changed event + && _presets.Any(p => p.Triggers.Any(t => t.Trigger == PresetTriggerType.ProcessSwitch || t.Trigger == PresetTriggerType.ScreensaverStart || t.Trigger == PresetTriggerType.ScreensaverStop) + && ((string.IsNullOrEmpty(p.DeviceMacAddress) && d == SelectedDevice) || p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase))))) + .ToList(); + + if (!applicableDevices.Any()) + { + return; + } + + if (_poweredOffByScreenSaver && !_sessionSwitchDispatcher.UserLocalSession) + { + return; + } + + var processes = args.RunningProcesses; + + if (_poweredOffByScreenSaver) + { + if (processes.Any(p => p.Id == _poweredOffByScreenSaverProcessId)) + { + return; + } + + var process = processes.FirstOrDefault(p => p.ProcessName.ToLowerInvariant().EndsWith(".scr")); + + if (process != null) + { + Logger.Debug($"Detected screen saver process switch from {_poweredOffByScreenSaverProcessId} to {process.Id}"); + + _poweredOffByScreenSaverProcessId = process.Id; + return; + } + + Logger.Debug("Screensaver stopped, waking"); + _poweredOffByScreenSaver = false; + _poweredOffByScreenSaverProcessId = 0; + + await WakeAfterStartupOrResume(PowerOnOffState.ScreenSaver); + + await ExecuteScreenSaverPresetsCustom(args, applicableDevices); + + return; + } + + var connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); + + if (!connectedDevices.Any()) + { + if (wasConnected == true) + { + Logger.Debug("Process monitor: TV(s) where connected, but not any longer"); + wasConnected = false; + + return; + } + else + { + var devices = applicableDevices.ToList(); + foreach (var device in devices) + { + Logger.Debug($"Process monitor: test connection with {device.Name}..."); + + var test = await device.TestConnection(); + Logger.Debug("Process monitor: test connection result: " + test); + } + + connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); + + if (!connectedDevices.Any()) + { + return; + } + } + } + + if (wasConnected == false) + { + Logger.Debug("Process monitor: TV(s) where not connected, but connection has now been established"); + wasConnected = true; + } + + if (connectedDevices.Any(d => d.PowerOffOnScreenSaver || d.PowerOnAfterScreenSaver)) + { + await HandleScreenSaverProcessAsync(processes, connectedDevices); + } + + await ExecuteProcessPresets(args, connectedDevices); + + await ExecuteScreenSaverPresetsCustom(args, connectedDevices); + } + catch (Exception ex) + { + Logger.Error("ProcessChanged: " + ex.ToLogString()); + } + } + + private async Task ExecuteScreenSaverPresetsCustom(ProcessChangedEventArgs context, IList connectedDevices) + { + var triggerDevices = connectedDevices.Where(d => d.TriggersEnabled).ToList(); + + if (!triggerDevices.Any()) + { + return; + } + + await ExecuteScreenSaverPresets(_serviceManager, context, triggerDevices.Any(d => d.IsUsingHDRPictureMode())); + } + + private async Task ExecuteProcessPresets(ProcessChangedEventArgs context, IList connectedDevices) + { + var triggerDevices = connectedDevices.Where(d => d.TriggersEnabled).ToList(); + + if (context.IsScreenSaverActive) + { + return; + } + + var selectedDevice = SelectedDevice; + + var presets = _presets.Where(p => p.Triggers.Any(t => t.Trigger == PresetTriggerType.ProcessSwitch) && + ((string.IsNullOrEmpty(p.DeviceMacAddress) && selectedDevice?.TriggersEnabled == true) + || Devices.Any(d => d.TriggersEnabled && p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase)))); + if (!presets.Any()) + { + return; + } + + if (context.ForegroundProcess != null && context.ForegroundProcessIsFullScreen) + { + presets = presets.Where(p => p.Triggers.Any(t => t.Conditions.HasFlag(PresetConditionType.FullScreen))); + } + else if (context.StoppedFullScreen) + { + presets = presets.Where(p => p.Triggers.Any(t => !t.Conditions.HasFlag(PresetConditionType.FullScreen))); + } + + var triggerContext = await CreateTriggerContext(_serviceManager, context, triggerDevices.Any(d => d.IsUsingHDRPictureMode())); + + var toApplyPresets = presets.Where(p => p.Triggers.Any(t => t.TriggerActive(triggerContext))).ToList(); + + if (toApplyPresets.Any()) + { + var toApplyPreset = toApplyPresets.FirstOrDefault(p => toApplyPresets.Count == 1 || p.Triggers.Any(t => !t.IncludedProcesses.Contains("*"))) ?? toApplyPresets.First(); + + if (_lastTriggeredPreset != toApplyPreset) + { + Logger.Debug($"Applying triggered preset: {toApplyPreset.name} because trigger conditions are met"); + + await ApplyPreset(toApplyPreset); + + _lastTriggeredPreset = toApplyPreset; + } + } + } + + private async Task HandleScreenSaverProcessAsync(IList processes, List connectedDevices) + { + var process = processes.FirstOrDefault(p => p.ProcessName.ToLowerInvariant().EndsWith(".scr")); + + var powerOffDevices = connectedDevices.Where(d => d.PowerOffOnScreenSaver).ToList(); + + if (process == null) + { + powerOffDevices.ForEach(d => d.ClearPowerOffTask()); + + return; + } + + var parent = process.Parent(processes); + + // Ignore manually started screen savers if no devices prefer this + if (parent?.ProcessName.Contains("winlogon") != true && !connectedDevices.Any(d => d.HandleManualScreenSaver)) + { + return; + } + + Logger.Debug($"Screensaver started: {process.ProcessName}, parent: {parent.ProcessName}"); + try + { + foreach (var device in powerOffDevices) + { + Logger.Debug($"Screensaver check: test connection with {device.Name}..."); + + var test = await device.TestConnection(); + Logger.Debug("Screensaver check: test connection result: " + test); + if (!test) + { + continue; + } + + if (device.ScreenSaverMinimalDuration > 0) + { + Logger.Debug($"Screensaver check: powering off tv {device.Name} in {device.ScreenSaverMinimalDuration} seconds because of screensaver"); + + _poweredOffByScreenSaver = true; + + device.PowerOffIn(device.ScreenSaverMinimalDuration); + + continue; + } + + Logger.Debug($"Screensaver check: powering off tv {device.Name} because of screensaver"); + try + { + _poweredOffByScreenSaver = await device.PerformActionOnScreenSaver().WaitAsync(TimeSpan.FromSeconds(5)); + } + catch (TimeoutException) + { + // Assume powering off was successful + _poweredOffByScreenSaver = true; + Logger.Error($"Screensaver check: timeout exception while powering off (probably connection closed)"); + } + catch (Exception pex) + { + // Assume powering off was successful + _poweredOffByScreenSaver = true; + Logger.Error($"Screensaver check: exception while powering off (probably connection closed): " + pex.Message); + } + Logger.Debug($"Screensaver check: powered off tv {device.Name}"); + } + + _poweredOffByScreenSaverProcessId = process.Id; + } + catch (Exception e) + { + Logger.Error($"Screensaver check: can't power off: " + e.ToLogString()); + } + } + + internal void AddCustomDevice(LgDevice device) + { + Devices.Add(device); + } + + internal void RemoveCustomDevice(LgDevice device) + { + Devices.Remove(device); + + if (!string.IsNullOrEmpty(device.MacAddress)) + { + var presets = _presets.Where(p => !string.IsNullOrEmpty(p.DeviceMacAddress) && p.DeviceMacAddress.Equals(device.MacAddress, StringComparison.OrdinalIgnoreCase)).ToList(); + presets.ForEach(preset => + { + preset.DeviceMacAddress = null; + }); + } + + if (SelectedDevice == device) + { + SelectedDevice = Devices.Any() ? Devices.First() : null; + } + } + + public void PowerOffDevices(IEnumerable devices, PowerOnOffState state = PowerOnOffState.ShutDown) + { + if (!devices.Any()) + { + return; + } + + if (NativeMethods.GetAsyncKeyState(NativeConstants.VK_CONTROL) < 0 || NativeMethods.GetAsyncKeyState(NativeConstants.VK_RCONTROL) < 0) + { + Logger.Debug("Not powering off because CONTROL-key is down"); + return; + } + + if (state == PowerOnOffState.ShutDown) + { + var sleep = Config.ShutdownDelay; + + Logger.Debug($"PowerOffDevices: Waiting for a maximum of {sleep} milliseconds..."); + + while (sleep > 0 && _restartDetector?.PowerOffDetected == false) + { + Thread.Sleep(100); + + if (_restartDetector != null && (_restartDetector.RestartDetected || _restartDetector.IsRebootInProgress())) + { + Logger.Debug("Not powering off because of a restart"); + return; + } + + sleep -= 100; + } + } + + Logger.Debug("Powering off tv(s)..."); + var tasks = new List(); + foreach (var device in devices) + { + var task = device.PowerOff(true, false); + + tasks.Add(task); + } + + if (state == PowerOnOffState.StandBy) + { + var standByScript = Path.Combine(Program.DataDir, "StandByScript.bat"); + if (File.Exists(standByScript)) + { + _winApiAdminService.StartProcess(standByScript, hidden: true, wait: true); + } + } + + // We can't use async here because we need to stay on the main thread... + var _ = Task.WhenAll(tasks.ToArray()).ConfigureAwait(true); + + Logger.Debug("Done powering off tv(s)"); + } + + public Task PowerOnDevicesTask(IEnumerable devices, PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) + { + return Task.Run(async () => await PowerOnDevices(devices, state, checkUserSession)); + } + + public async Task PowerOnDevices(IEnumerable devices, PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) + { + if (!devices.Any()) + { + return; + } + + if (checkUserSession && !_sessionSwitchDispatcher.UserLocalSession) + { + Logger.Debug($"WakeAfterStartupOrResume: not waking because session info indicates no local session"); + return; + } + + var resumeScript = Path.Combine(Program.DataDir, "ResumeScript.bat"); + + var tasks = new List(); + + foreach (var device in devices) + { + if (!device.PowerOnAfterManualPowerOff && device.PoweredOffBy == LgDevice.PowerOffSource.Manually) + { + Logger.Debug($"[{device.Name}]: device was manually powered off by user, not powering on"); + continue; + } + + if (state == PowerOnOffState.ScreenSaver) + { + tasks.Add(device.PerformActionAfterScreenSaver(Config.PowerOnRetries)); + continue; + } + + var task = device.WakeAndConnectWithRetries(Config.PowerOnRetries); + tasks.Add(task); + } + + await Task.WhenAll(tasks.ToArray()); + + if (File.Exists(resumeScript)) + { + _winApiAdminService.StartProcess(resumeScript, hidden: true); + } + } + + private void SendConfigToService() + { + if (!_winApiService.IsServiceRunning()) + { + return; + } + + var json = JsonConvert.SerializeObject(Config); + + var message = new SvcMessage + { + MessageType = SvcMessageType.SetLgConfig, + Data = json + }; + + PipeUtils.SendMessage(message); + } + + public async Task ExecuteEventPresets(PresetTriggerType triggerType) + { + await ExecuteEventPresets(_serviceManager, new[] { triggerType }); + } + + public void ToggleGameBar() + { + if (_gameBarForm == null || !_gameBarForm.Visible) + { + if (_gameBarForm == null || _gameBarForm.IsDisposed) + { + if (SelectedDevice == null) + { + return; + } + + _gameBarForm = new LgGameBar(this); + } + + _gameBarForm.Show(); + _gameBarForm.Activate(); + } + else + { + _gameBarForm?.Hide(); + } + } + + public List GetDevices() + { + if (Devices == null) + { + return new List(); + } + + return Devices.Select(d => new LgDeviceDto + { + Name = d.Name, + MacAddress = d.MacAddress, + IpAddress = d.IpAddress, + IsCustom = d.IsCustom, + IsDummy = d.IsDummy, + Options = new LgDeviceOptions + { + HandleManualScreenSaver = d.HandleManualScreenSaver, + HDMIPortNumber = d.HDMIPortNumber, + PowerOffByWindows = d.PowerOffByWindows, + PowerOffOnScreenSaver = d.PowerOffOnScreenSaver, + PowerOffOnShutdown = d.PowerOffOnShutdown, + PowerOffOnStandby = d.PowerOffOnStandby, + PowerOnAfterManualPowerOff = d.PowerOnAfterManualPowerOff, + PowerOnAfterResume = d.PowerOnAfterResume, + PowerOnAfterScreenSaver = d.PowerOnAfterScreenSaver, + PowerOnAfterStartup = d.PowerOnAfterStartup, + PowerOnByWindows = d.PowerOnByWindows, + PowerOffAfterSessionLock = d.PowerOffAfterSessionLock, + PowerOnAfterSessionUnlock = d.PowerOnAfterSessionUnlock, + ScreenSaverMinimalDuration = d.ScreenSaverMinimalDuration, + TriggersEnabled = d.TriggersEnabled, + TurnScreenOffOnScreenSaver = d.TurnScreenOffOnScreenSaver, + TurnScreenOnAfterScreenSaver = d.TurnScreenOnAfterScreenSaver, + UseSecureConnection = d.UseSecureConnection + }, + IsConnected = d.IsConnected(), + IsSelected = d == SelectedDevice + }).ToList(); + } + + public async Task UpdateDevice(LgDeviceDto deviceSpec) + { + if (Devices == null) + { + return false; + } + + var device = Devices.FirstOrDefault(d => d.MacAddress == deviceSpec.MacAddress); + + if (device == null) + { + device = new LgDevice(deviceSpec); + + Devices.Add(device); + } + else + { + var reconnect = device.Update(deviceSpec); + + if (reconnect) + { + await device.Connect(); + } + } + + SaveConfig(); + + return true; + } + + public async Task TestDevice(LgDeviceDto deviceSpec) + { + if (Devices == null) + { + return false; + } + + try + { + var device = new LgDevice(deviceSpec); + + return await device.TestConnection(); + } + catch + { + return false; + } + } + + public LgServiceConfigDto GetConfig() + { + return new LgServiceConfigDto + { + DefaultButtonDelay = Config.DefaultButtonDelay, + DeviceSearchKey = Config.DeviceSearchKey, + //Devices = GetDevices(), + PowerOnAfterStartup = Config.PowerOnAfterStartup, + PowerOnRetries = Config.PowerOnRetries, + PreferredMacAddress = Config.PreferredMacAddress, + QuickAccessShortcut = Config.QuickAccessShortcut, + SetSelectedDeviceByPowerOn = Config.SetSelectedDeviceByPowerOn, + ShowAdvancedActions = Config.ShowAdvancedActions, + ShutdownDelay = Config.ShutdownDelay, + GameBarShortcut = Config.GameBarShortcut, + GameBarShowingTime = Config.GameBarShowingTime + }; + } + + public bool UpdateConfig(LgServiceConfigDto config) + { + Config.Update(config); + + SetShortcuts(SHORTCUTID_LGQA, Config.QuickAccessShortcut); + + SaveConfig(); + + return true; + } + + public bool UpdatePreset(LgPreset specPreset) + { + ValidatePreset(specPreset); + + var currentPreset = _presets.FirstOrDefault(p => p.id == specPreset.id); + + if (currentPreset != null) + { + currentPreset.Update(specPreset); + + SavePresets(currentPreset); + return true; + } + + var newPreset = new LgPreset(specPreset); + newPreset.name = specPreset.name; + + _presets.Add(newPreset); + + SavePresets(newPreset); + + return true; + } + + public List> GetInvokableActions() + { + return SelectedDevice?.GetInvokableActions(Config.ShowAdvancedActions).Select(a => new InvokableActionDto + { + Advanced = a.Advanced, + Category = Utils.FirstCharUpperCase(a.Category ?? "Misc"), + CurrentValue = a.CurrentValue, + EnumType = a.EnumType, + MaxValue = a.MaxValue, + MinValue = a.MinValue, + Name = a.Name, + NumberOfValues = a.NumberOfValues, + Preset = a.Preset, + Title = a.Title ?? Utils.FirstCharUpperCase(a.Name), + ValueLabels = a.ValueLabels + }).ToList(); + } + + public async Task ExecuteInvokableAction(InvokableActionDto invokableAction, List parameters = null) + { + var device = SelectedDevice; + + if (invokableAction.SelectedDevice != null) + { + device = MatchDevice(invokableAction.SelectedDevice) ?? SelectedDevice; + } + + if (invokableAction.Preset != null) + { + await SelectedDevice?.ExecutePreset(invokableAction.Preset, false, Config); + + return true; + } + + return await device?.ExecuteInvokableAction(invokableAction, parameters); + } + + public bool SetSelectedDevice(LgDeviceDto deviceDto) + { + if (deviceDto == null) + { + return false; + } + + var device = MatchDevice(deviceDto); + if (device == null) + { + return false; + } + + SelectedDevice = device; + + return true; + } + + private LgDevice MatchDevice(LgDeviceDto deviceDto) + { + return Devices?.FirstOrDefault(d => string.IsNullOrEmpty(deviceDto.MacAddress) ? d.IpAddress == deviceDto.IpAddress : d.MacAddress == deviceDto.MacAddress); + } + } } \ No newline at end of file diff --git a/ColorControl/Services/NVIDIA/NvService.cs b/ColorControl/Services/NVIDIA/NvService.cs index abe87e3..1c25fc1 100644 --- a/ColorControl/Services/NVIDIA/NvService.cs +++ b/ColorControl/Services/NVIDIA/NvService.cs @@ -300,6 +300,8 @@ namespace ColorControl.Services.NVIDIA public bool UpdatePreset(NvPreset specPreset) { + ValidatePreset(specPreset); + var currentPreset = _presets.FirstOrDefault(p => p.id == specPreset.id); if (currentPreset != null) @@ -308,7 +310,7 @@ namespace ColorControl.Services.NVIDIA currentPreset.shortcut = specPreset.shortcut; currentPreset.Update(specPreset); - SavePresets(); + SavePresets(currentPreset); return true; } @@ -319,7 +321,7 @@ namespace ColorControl.Services.NVIDIA _presets.Add(newPreset); - SavePresets(); + SavePresets(newPreset); return true; } @@ -858,13 +860,7 @@ namespace ColorControl.Services.NVIDIA var gpuHandle = displayDevice.PhysicalGPU.Handle; var displayId = displayDevice.DisplayId; - if (state == NvDitherState.Auto) - { - // These seem to be ignored in Auto-state - bits = 0; - mode = 0; - } - else if (preset != null) + if (preset != null) { bits = preset.ditheringBits; mode = preset.ditheringMode; @@ -873,6 +869,13 @@ namespace ColorControl.Services.NVIDIA restartDriver |= preset.RestartDriver; } + if (state == NvDitherState.Auto) + { + // These seem to be ignored in Auto-state + bits = 0; + mode = 0; + } + if (setRegistryKey) { var info = CCD.GetDisplayInfo(display.Name); @@ -1566,7 +1569,7 @@ namespace ColorControl.Services.NVIDIA return _meta.GetSettingMeta(settingId); } - public List GetDriverSettings(string profileName = null) + public List GetDriverSettings(string profileName = null, bool addFriendlyName = false) { if (profileName == null) { @@ -1599,7 +1602,8 @@ namespace ColorControl.Services.NVIDIA Value = intValue.intValue, DefaultValue = intValue.defaultValue, SettingType = type, - Values = values?.ToList() + Values = values?.ToList(), + FriendlyName = addFriendlyName ? $"{settingMeta.SettingName}: {settingMeta.ToFriendlyName(intValue: intValue.intValue, displayDefault: true)}" : null }; }).ToList(); } diff --git a/ColorControl/Services/Samsung/SamTvConnection.cs b/ColorControl/Services/Samsung/SamTvConnection.cs index 6e50ba6..8b81b2c 100644 --- a/ColorControl/Services/Samsung/SamTvConnection.cs +++ b/ColorControl/Services/Samsung/SamTvConnection.cs @@ -1,13 +1,11 @@ -using ColorControl.Shared.Common; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; +using Newtonsoft.Json; using System; -using System.Collections.Concurrent; +using System.Net.Security; +using System.Net.WebSockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Threading; using System.Threading.Tasks; -using Windows.Networking.Sockets; -using Windows.Security.Cryptography.Certificates; -using Windows.Storage.Streams; namespace ColorControl.Services.Samsung { @@ -21,72 +19,53 @@ namespace ColorControl.Services.Samsung public delegate void ConnectedDelegate(string token, bool disconnect); - public class SamTvConnection : IDisposable + public class SamTvConnection : IAsyncDisposable { - public static SynchronizationContext SyncContext { get; set; } - public bool? ConnectionClosed { get; private set; } public bool ClosedByDispose { get; private set; } - private MessageWebSocket _connection; - private DataWriter _messageWriter; + private ClientWebSocket _clientWebSocket; private int _commandCount; - private int _readTimeout; + private Uri _uri; public event ConnectedDelegate Connected; - private readonly ConcurrentDictionary> _tokens = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary> _callbacks = new ConcurrentDictionary>(); private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - public async Task Connect(Uri uri, int readTimeout = 5000) + public async Task Connect(Uri uri, int connectTimeout = 5000) { - _readTimeout = readTimeout; + _uri = uri; try { - _connection?.Dispose(); - + _clientWebSocket?.Dispose(); _commandCount = 0; - // Not creating the web socket from main thread can cause a stackoverflow in combase32.dll - if (Environment.CurrentManagedThreadId != 1) - { - SyncContext.Send((_) => CreateWebSocket(), null); - } - else - { - CreateWebSocket(); - } + CreateWebSocket(); try { - await _connection.ConnectAsync(uri).AsTask().WaitAsync(TimeSpan.FromSeconds(5)); + Logger.Debug($"Connecting to {uri}"); + await _clientWebSocket.ConnectAsync(uri, CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(connectTimeout)); + + Logger.Debug($"Connected to {uri}"); ConnectionClosed = false; - _messageWriter = new DataWriter(_connection.OutputStream); + await Receive(true); + + Logger.Debug("Received"); return true; } catch (TimeoutException te) { - Logger.Error($"Connect to {uri}: {te.Message}"); + Logger.Error($"Timeout while connecting to {uri}: {te.Message}"); return false; } } catch (Exception e) { - switch (SocketError.GetStatus(e.HResult)) - { - case SocketErrorStatus.HostNotFound: - // Handle HostNotFound Error - break; - default: - // Handle Unknown Error - break; - } - Logger.Error($"Connect to {uri}: {e.Message}"); return false; } @@ -94,63 +73,50 @@ namespace ColorControl.Services.Samsung private void CreateWebSocket() { - _connection = new MessageWebSocket(); - _connection.Control.IgnorableServerCertificateErrors.Add(ChainValidationResult.Untrusted); - _connection.Control.IgnorableServerCertificateErrors.Add(ChainValidationResult.InvalidName); - _connection.Control.MessageType = SocketMessageType.Utf8; - _connection.MessageReceived += Connection_MessageReceived; - _connection.Closed += Connection_Closed; + _clientWebSocket = new ClientWebSocket(); + _clientWebSocket.Options.RemoteCertificateValidationCallback += CertificationValidationCallback; } - public async Task SendMessageAsync(string message) + private bool CertificationValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors) => true; + + public async Task SendMessageAsync(string message, bool reconnect = true) { try { - if (_messageWriter == null) + if (_clientWebSocket?.State != WebSocketState.Open) { - throw new Exception("SendMessageAsync: no writer"); + throw new InvalidOperationException("Socket not open"); } + var bytes = UTF8Encoding.UTF8.GetBytes(message); + var data = bytes.AsMemory(); - _messageWriter.WriteString(message); - - await _messageWriter.StoreAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5)); + await _clientWebSocket.SendAsync(data, WebSocketMessageType.Text, true, CancellationToken.None); + } + catch (WebSocketException) + { + if (reconnect && await Connect(_uri)) + { + await SendMessageAsync(message, false); + } } catch (Exception e) { Logger.Error("SendMessageAsync: " + e.Message); ConnectionClosed = true; - _messageWriter?.Dispose(); } } - private async Task SendCommandAsync(string id, string message, bool waitForAnswer = true) + private async Task SendCommandAsync(string message) { try { - if (waitForAnswer) + await SendMessageAsync(message); + if (ConnectionClosed == true) { - var taskSource = new TaskCompletionSource(); - _tokens.TryAdd(id, taskSource); - - await SendMessageAsync(message); - if (ConnectionClosed == true) - { - throw new Exception("Connection closed"); - } - - return await taskSource.Task; - } - else - { - await SendMessageAsync(message); - if (ConnectionClosed == true) - { - throw new Exception("Connection closed"); - } - - return null; + throw new Exception("Connection closed"); } + return null; } catch (Exception e) { @@ -158,7 +124,7 @@ namespace ColorControl.Services.Samsung } } - public async Task SendCommandAsync(RequestMessage message, bool waitForAnswer = true) + public async Task SendCommandAsync(RequestMessage message) { var rawMessage = new RawRequestMessage(message, ++_commandCount); var serialized = JsonConvert.SerializeObject(rawMessage, new JsonSerializerSettings() @@ -166,32 +132,7 @@ namespace ColorControl.Services.Samsung NullValueHandling = NullValueHandling.Ignore, //ContractResolver = new CamelCasePropertyNamesContractResolver() }); - return await SendCommandAsync(rawMessage.Id, serialized, waitForAnswer); - } - - public async Task SubscribeAsync(RequestMessage message, Func callback) - { - var rawMessage = new RawRequestMessage(message, ++_commandCount); - var serialized = JsonConvert.SerializeObject(rawMessage, new JsonSerializerSettings() - { - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); - - _callbacks.TryAdd(rawMessage.Id, callback); - - return await SendCommandAsync(rawMessage.Id, serialized); - } - - private void Connection_Closed(IWebSocket sender, WebSocketClosedEventArgs args) - { - var webSocket = Interlocked.Exchange(ref _connection, null); - webSocket?.Dispose(); - ConnectionClosed = true; - - Logger.Debug("Connection closed"); - - Connected?.Invoke(null, true); + return await SendCommandAsync(serialized); } private const string MS_CHANNEL_CONNECT_EVENT = "ms.channel.connect"; @@ -201,128 +142,76 @@ namespace ColorControl.Services.Samsung private const string MS_CHANNEL_UNAUTHORIZED = "ms.channel.unauthorized"; private const string MS_ERROR_EVENT = "ms.error"; - private async void Connection_MessageReceived(MessageWebSocket sender, MessageWebSocketMessageReceivedEventArgs args) + private async Task Receive(bool singleResponse) { - try - { - var task = new Task(args.GetDataReader); - task.Start(); - var dataReader = await task.WaitAsync(TimeSpan.FromMilliseconds(_readTimeout)); + var dataPerPacket = 8 * 1024; //8192 + var strBuilder = new StringBuilder(); + var buffer = new ArraySegment(new byte[dataPerPacket]); - using (dataReader) + while (_clientWebSocket?.State == WebSocketState.Open) + { + var receiveResult = await _clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); + + strBuilder.Append(Encoding.UTF8.GetString(buffer.Array, buffer.Offset, receiveResult.Count)); + //keep doing until end of message + while (!receiveResult.EndOfMessage) { - dataReader.UnicodeEncoding = UnicodeEncoding.Utf8; - var message = dataReader.ReadString(dataReader.UnconsumedBufferLength); - var obj = JsonConvert.DeserializeObject(message); - var id = (string)obj.id; - var type = (string)obj.@event; - - Logger.Debug($"Received message: {message}"); - - // Response: - // {"data":{"clients":[{"attributes":{"name":"c2Ftc3VuZ2N0bA=="},"connectTime":1685302606520,"deviceName":"c2Ftc3VuZ2N0bA==","id":"92e0cbc1-6f86-4013-9b9b-65a47afb2236","isHost":false}],"id":"92e0cbc1-6f86-4013-9b9b-65a47afb2236","token":"16224598"},"event":"ms.channel.connect"} - // Second response: - // {"data":{"clients":[{"attributes":{"name":"c2Ftc3VuZ2N0bA==","token":"16224598"},"connectTime":1685303321571,"deviceName":"c2Ftc3VuZ2N0bA==","id":"58589451-2ecf-44f0-9267-31983a5466","isHost":false}],"id":"58589451-2ecf-44f0-9267-31983a5466"},"event":"ms.channel.connect"} - - //TaskCompletionSource taskCompletion; - if (type == MS_CHANNEL_CONNECT_EVENT) - { - var token = (string)obj.data?.token; - - if (token == null) - { - token = (string)obj.data?.clients[0]?.attributes?.token; - } - - if (token != null) - { - Connected?.Invoke(token, false); - } - - //if (_tokens.TryRemove(id, out taskCompletion)) - //{ - // var key = (string)JObject.Parse(message)["payload"]["client-key"]; - // taskCompletion.TrySetResult(new { clientKey = key }); - //} - - } - else - { - - } - //else if (_tokens.TryGetValue(id, out taskCompletion)) - //{ - // if (id == "register_0") return; - // if (obj.type == "error") - // { - // taskCompletion.SetException(new Exception(obj.error?.ToString())); - // } - // //else if (args.Cancelled) - // //{ - // // taskSource.SetCanceled(); - // //} - // taskCompletion.TrySetResult(obj.payload); - - // if (_callbacks.TryGetValue(id, out Func callback)) - // { - // try - // { - // callback(obj.payload); - // } - // catch (Exception callbackException) - // { - // Logger.Error($"Connection_MessageReceived: the callback threw an exception: {callbackException.ToLogString()}"); - // } - // } - //} + receiveResult = await _clientWebSocket.ReceiveAsync(buffer, CancellationToken.None); + strBuilder.Append(Encoding.UTF8.GetString(buffer.Array, buffer.Offset, receiveResult.Count)); } - } - catch (Exception ex) - { - var status = WebSocketError.GetStatus(ex.GetBaseException().HResult); - Logger.Error($"Connection_MessageReceived: status: {status}, exception: {ex.ToLogString()}"); - SetExceptionOnAllTokens(ex); + var message = strBuilder.ToString(); + strBuilder.Clear(); - ConnectionClosed = true; + var obj = JsonConvert.DeserializeObject(message); + var id = (string)obj.id; + var type = (string)obj.@event; - _messageWriter?.Dispose(); + // Response: + // {"data":{"clients":[{"attributes":{"name":"c2Ftc3VuZ2N0bA=="},"connectTime":1685302606520,"deviceName":"c2Ftc3VuZ2N0bA==","id":"92e0cbc1-6f86-4013-9b9b-65a47afb2236","isHost":false}],"id":"92e0cbc1-6f86-4013-9b9b-65a47afb2236","token":"16224598"},"event":"ms.channel.connect"} + // Second response: + // {"data":{"clients":[{"attributes":{"name":"c2Ftc3VuZ2N0bA==","token":"16224598"},"connectTime":1685303321571,"deviceName":"c2Ftc3VuZ2N0bA==","id":"58589451-2ecf-44f0-9267-31983a5466","isHost":false}],"id":"58589451-2ecf-44f0-9267-31983a5466"},"event":"ms.channel.connect"} + + if (type == MS_CHANNEL_CONNECT_EVENT) + { + var token = (string)obj.data?.token; + + if (token == null) + { + token = (string)obj.data?.clients[0]?.attributes?.token; + } + + if (token != null) + { + Connected?.Invoke(token, false); + } + } + + if (singleResponse) + { + return; + } } } - private void SetExceptionOnAllTokens(Exception ex) - { - //Logger.Debug($"Tokens: {_tokens.Count}"); - - foreach (var taskCompletion in _tokens.Values) - { - try - { - if (taskCompletion.Task.Status != TaskStatus.RanToCompletion) - { - Logger.Debug("taskCompletion.Task.Status: " + taskCompletion.Task.Status); - taskCompletion.SetException(new Exception(ex.Message)); - } - } - catch (Exception ex2) - { - Logger.Error("SetExceptionOnAllTokens: " + ex2.Message); - } - } - - _tokens.Clear(); - } - - public void Close() - { - _connection?.Close(1000, ""); - } - - public void Dispose() + public async ValueTask DisposeAsync() { ClosedByDispose = true; - Close(); - _connection?.Dispose(); + + if (_clientWebSocket != null) + { + if (_clientWebSocket.State == WebSocketState.Open) + { + try + { + await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + } + catch { } + } + _clientWebSocket?.Dispose(); + } + + GC.SuppressFinalize(this); } } } diff --git a/ColorControl/Services/Samsung/SamsungDevice.cs b/ColorControl/Services/Samsung/SamsungDevice.cs index b45f1d4..b721f4b 100644 --- a/ColorControl/Services/Samsung/SamsungDevice.cs +++ b/ColorControl/Services/Samsung/SamsungDevice.cs @@ -15,655 +15,672 @@ using System.Threading.Tasks; namespace ColorControl.Services.Samsung { - public enum PowerOffSource - { - Unknown, - App, - External - } - - public delegate void GenericDelegate(object sender); - - class SamsungDevice - { - public class InvokableAction - { - public Func, Task> AsyncFunction { get; set; } - public string Name { get; set; } - public Type EnumType { get; set; } - public decimal MinValue { get; set; } - public decimal MaxValue { get; set; } - public string Category { get; set; } - public string Title { get; set; } - public int CurrentValue { get; set; } - public int NumberOfValues { get; set; } - public bool Advanced { get; set; } - public SamsungPreset Preset { get; set; } - public List ValueLabels { get; set; } - } - - - private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - - public string Name { get; private set; } - public string IpAddress { get; private set; } - public string MacAddress { get; private set; } - public bool IsCustom { get; private set; } - public string Token { get; set; } - public SamsungDeviceOptions Options { get; set; } - - [JsonIgnore] - public bool IsDummy { get; private set; } - - [JsonIgnore] - public bool PoweredOn { get; private set; } - [JsonIgnore] - public DateTimeOffset PoweredOffAt { get; private set; } - [JsonIgnore] - public PowerOffSource PoweredOffBy { get; internal set; } - - public event GenericDelegate Connected; - - [JsonIgnore] - private SamTvConnection _samTvConnection; - - [JsonIgnore] - private ServiceManager _serviceManager; - [JsonIgnore] - private Timer _powerOffTimer; - [JsonIgnore] - private SemaphoreSlim _connectSemaphore = new SemaphoreSlim(1); - [JsonIgnore] - private TaskCompletionSource _taskCompletionSource; - - [JsonIgnore] - private List _invokableActions = new List(); - - [JsonConstructor] - public SamsungDevice(string name, string ipAddress, string macAddress, bool isCustom = true, bool isDummy = false) - { - Options = new SamsungDeviceOptions(); - - Name = name; - IpAddress = ipAddress; - MacAddress = macAddress; - IsCustom = isCustom; - IsDummy = isDummy; - - AddInvokableAction("WOL", WakeAction); - AddInvokableAction("ServiceMenu", ServiceMenuAction, true); - AddInternalPresetAction(new SamsungPreset("PictureOff", null, new[] { "KEY_AD:1400", "KEY_DOWN:200", "KEY_DOWN:200", "KEY_ENTER" })); - AddInternalPresetAction(new SamsungPreset("PictureOn", null, new[] { "KEY_RETURN:1000", "KEY_RETURN", "KEY_RETURN" })); - - _serviceManager = Program.ServiceProvider.GetRequiredService(); - } - - public SamsungDevice(SamsungDeviceDto spec) : this(spec.Name, spec.IpAddress, spec.MacAddress, spec.IsCustom) - { - // TODO: call update method - if (spec.Options != null) - { - Options = spec.Options; - } - } - - ~SamsungDevice() - { - ClearPowerOffTask(); - } - - public bool Update(SamsungDeviceDto spec) - { - var reconnectNeeded = false; - - Name = spec.Name; - - if (IpAddress != spec.IpAddress) - { - reconnectNeeded = IsConnected(); - IpAddress = spec.IpAddress; - } - - MacAddress = spec.MacAddress; - IsCustom = spec.IsCustom; - - // TODO: call update method - if (spec.Options != null) - { - Options = spec.Options; - } - - return reconnectNeeded; - } - - public async Task ConnectAsync(bool force = false, bool retryToken = false) - { - if (!retryToken) - { - await _connectSemaphore.WaitAsync(); - } - try - { - if (force) - { - _samTvConnection?.Dispose(); - _samTvConnection = null; - } - else if (IsConnected()) - { - return true; - } - - _samTvConnection = new SamTvConnection(); - _samTvConnection.Connected += Connection_Connected; - var b64 = Utils.Base64Encode("ColorControl"); - - var firstTime = Token == null; - var readTimeout = firstTime ? 60000 : 5000; - - try - { - var protocol = Options.UseSecureConnection ? "wss" : "ws"; - var port = Options.UseSecureConnection ? 8002 : 8001; - - var uriString = $"{protocol}://{IpAddress}:{port}/api/v2/channels/samsung.remote.control?name={b64}"; - - if (Token != null) - { - uriString += $"&token={Token}"; - } - - var result = await _samTvConnection.Connect(new Uri(uriString), readTimeout); - - if (!result) - { - return false; - } - - var waitTime = Token != null ? 3000 : 60000; - - _taskCompletionSource = new TaskCompletionSource(); - - await _taskCompletionSource.Task.WaitAsync(TimeSpan.FromMilliseconds(waitTime)); - - _taskCompletionSource = null; - - if (firstTime && Token != null && !retryToken) - { - _samTvConnection.Dispose(); - _samTvConnection = null; - - return await ConnectAsync(force, true); - } - - return true; - } - catch (Exception ex) - { - Logger.Debug(ex); - - return false; - } - - } - finally - { - if (!retryToken) - { - _connectSemaphore.Release(); - } - - if (true) - { - Connected?.Invoke(this); - } - } - } - - private void Connection_Connected(string token, bool disconnect) - { - if (disconnect) - { - Logger.Debug($"Disconnect event, powered off by: {PoweredOffBy}"); - - if (_samTvConnection?.ClosedByDispose != true && PoweredOffBy == PowerOffSource.Unknown) - { - PoweredOffBy = PowerOffSource.External; - } - - return; - } - - Token = token; - - _taskCompletionSource?.SetResult(token); - PoweredOffBy = PowerOffSource.Unknown; - } - - internal async Task PowerOffAsync() - { - try - { - await SendKeyAsync("KEY_POWER").WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(true); - } - catch (TimeoutException ex) - { - Logger.Debug($"Timeout when turning off tv: {ex.Message}"); - } - - PoweredOn = false; - PoweredOffAt = DateTimeOffset.Now; - PoweredOffBy = PowerOffSource.App; - } - - internal async Task WakeAndConnectWithRetries(int retries = 5) - { - var maxRetries = retries <= 1 ? 5 : retries; - - var result = false; - for (var retry = 0; retry < maxRetries && !result; retry++) - { - Logger.Debug($"WakeAndConnectWithRetries: attempt {retry + 1} of {maxRetries}..."); - - var ms = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - result = await WakeAndConnect(); - ms = DateTimeOffset.Now.ToUnixTimeMilliseconds() - ms; - if (!result) - { - var delay = 2000 - ms; - if (delay > 0) - { - await Task.Delay((int)delay); - } - } - } - - return result; - } - - internal async Task WakeAndConnect(int connectDelay = 500) - { - try - { - var result = await Utils.PingHost(IpAddress); - - Logger.Debug($"Ping result: {result}"); - - var powerState = default(string); - - if (result) - { - powerState = await GetPowerStateAsync(); - } - - if (result && powerState == "on" && IsConnected()) - { - Logger.Debug("Already powered on and connected"); - return true; - } - - if (!result || string.IsNullOrEmpty(powerState)) - { - result = Wake(); - if (!result) - { - Logger.Debug("WOL failed"); - return false; - } - Logger.Debug("WOL succeeded"); - await Task.Delay(connectDelay); - } - - result = await ConnectAsync(true); - - if (result && powerState == "standby") - { - await SendKeyAsync("KEY_POWER"); - //var powerState = await GetPowerStateAsync(); - - ////if (!PoweredOn && (PoweredOffAt == DateTimeOffset.MinValue || PoweredOffAt >= DateTimeOffset.Now.AddMinutes(-2))) - //if (powerState == "standby") - //{ - // await SendKeyAsync("KEY_POWER"); - //} - } - - PoweredOn = true; - - Logger.Debug("Connect succeeded: " + result); - return result; - } - catch (Exception e) - { - Logger.Error("WakeAndConnectToSelectedDevice: " + e.ToLogString()); - return false; - } - } - - internal bool Wake() - { - var result = false; - - if (MacAddress != null) - { - var wolService = Program.ServiceProvider.GetRequiredService(); - - result = wolService.SendWol(MacAddress, IpAddress); - } - else - { - Logger.Debug("Cannot wake device: the device has no MAC-address"); - } - - return result; - } - - public async Task GetPowerStateAsync() - { - var url = $"http://{IpAddress}:8001/api/v2/"; - - var json = await Utils.GetRestJsonAsync(url); - - if (json == null) - { - Logger.Error("No power state received"); - - return string.Empty; - } - - var powerState = (string)json.device?.PowerState; - - Logger.Debug($"Current power state: {powerState}"); - - return powerState; - } - - internal async Task TestConnectionAsync() - { - var result = await Utils.PingHost(IpAddress); - - if (result) - { - return await ConnectAsync(); - } - - return false; - } - - public bool IsConnected() - { - return _samTvConnection?.ConnectionClosed == false; - } - - internal void ConvertToCustom() - { - IsCustom = true; - } - - public override string ToString() - { - return $"{(IsDummy ? string.Empty : (IsCustom ? "Custom: " : "Auto detect: "))}{Name}{(!string.IsNullOrEmpty(IpAddress) ? ", " + IpAddress : string.Empty)}"; - } - - internal async Task ExecutePresetAsync(SamsungPreset preset, Shared.Common.GlobalContext appContext = null, SamsungServiceConfig config = null) - { - if (config == null) - { - var service = Program.ServiceProvider.GetRequiredService(); - config = service.Config; - } - - var hasApp = !string.IsNullOrEmpty(preset.AppId); - - var hasWOL = preset.Steps.Any(s => s.Equals("WOL", StringComparison.OrdinalIgnoreCase)); - - if (hasWOL) - { - var connected = await WakeAndConnect(); - if (!connected) - { - return false; - } - } - - for (var tries = 0; tries <= 1; tries++) - { - if (!await ConnectAsync()) - { - return false; - } - - if (hasApp) - { - try - { - //await _lgTvApi.LaunchApp(preset.appId, @params); - } - catch (Exception ex) - { - string logMessage = ex.ToLogString(Environment.StackTrace); - Logger.Error("Error while launching app: " + logMessage); - - if (tries == 0) - { - continue; - } - return false; - } - - await Task.Delay(1000); - } - - if (preset.Steps.Any()) - { - if (hasApp) - { - await Task.Delay(1500); - } - try - { - await ExecuteStepsAsync(preset, appContext, config); - } - catch (Exception ex) - { - string logMessage = ex.ToLogString(Environment.StackTrace); - Logger.Error("Error while executing steps: " + logMessage); - - if (tries == 0) - { - continue; - } - return false; - } - } - - return true; - } - - return true; - } - - private async Task ExecuteStepsAsync(SamsungPreset preset, Shared.Common.GlobalContext appContext, SamsungServiceConfig config) - { - foreach (var step in preset.Steps) - { - var keySpec = step.Split(':'); - - var delay = config.DefaultButtonDelay; - var cmd = "Click"; - var key = step; - if (keySpec.Length >= 2) - { - delay = Utils.ParseInt(keySpec[1]); - if (delay > 0) - { - key = keySpec[0]; - } - } - if (keySpec.Length >= 3) - { - cmd = keySpec[2]; - } - - var index = key.IndexOf("("); - string[] parameters = null; - if (index > -1) - { - var keyValue = key.Split('('); - key = keyValue[0]; - parameters = keyValue[1].Substring(0, keyValue[1].Length - 1).Split(';'); - } - - var executeKey = true; - var action = _invokableActions.FirstOrDefault(a => a.Name.Equals(key, StringComparison.OrdinalIgnoreCase)); - if (action != null) - { - await ExecuteActionAsync(action, parameters, appContext, config); - - executeKey = false; - } - - if (parameters != null && await _serviceManager.HandleExternalServiceAsync(key, parameters)) - { - executeKey = false; - } - - if (executeKey) - { - await SendKeyAsync(key, cmd); - delay = delay == 0 ? config.DefaultButtonDelay : delay; - } - - if (delay > 0) - { - await Task.Delay(delay); - } - } - } - - public async Task ExecuteActionAsync(InvokableAction action, string[] parameters, Shared.Common.GlobalContext appContext, SamsungServiceConfig config) - { - if (action.Preset != null) - { - await ExecutePresetAsync(action.Preset, appContext, config); - - return; - } - if (action.AsyncFunction != null) - { - await action.AsyncFunction(null); - } - } - - private async Task SendKeyAsync(string key, string cmd = "Click") - { - var dynamic = new - { - Cmd = cmd, - DataOfCmd = key, - Option = "false", - TypeOfRemote = "SendRemoteKey" - }; - - var message = new RequestMessage("ms.remote.control", dynamic); - - await _samTvConnection.SendCommandAsync(message, false); - } - - internal void ClearPowerOffTask() - { - _powerOffTimer?.Dispose(); - } - - internal void PowerOffIn(int seconds) - { - ClearPowerOffTask(); - - _powerOffTimer = new Timer(PowerOffByTimer); - _powerOffTimer.Change(TimeSpan.FromSeconds(seconds), TimeSpan.FromSeconds(seconds)); - } - - private void PowerOffByTimer(object _) - { - ClearPowerOffTask(); - - var x = PerformActionOnScreenSaver(); - } - - private bool IsPowerOffAllowed() => true; - - public async Task PerformActionOnScreenSaver() - { - if (!IsPowerOffAllowed()) - { - return true; - } - - if (Options.TurnScreenOffOnScreenSaver) - { - Logger.Debug("Turning screen off on screen saver"); - await TurnPictureOff(); - - return true; - } - - Logger.Debug($"Device {Name} is now powering off due to delayed screensaver task"); - - await PowerOffAsync(); - - return true; - } - - private async Task TurnPictureOff() - { - var action = _invokableActions.FirstOrDefault(a => a.Name == "PictureOff"); - - if (action?.Preset != null) - { - await ExecutePresetAsync(action.Preset); - } - } - - private async Task TurnPictureOn() - { - var action = _invokableActions.FirstOrDefault(a => a.Name == "PictureOn"); - - if (action?.Preset != null) - { - await ExecutePresetAsync(action.Preset); - } - } - - public async Task PerformActionAfterScreenSaver(int powerOnRetries) - { - if (Options.TurnScreenOffOnScreenSaver && IsConnected()) - { - if (Options.TurnScreenOnAfterScreenSaver) - { - try - { - Logger.Debug("Turning screen on after screen saver"); - await TurnPictureOn(); - } - catch (Exception) { } - } - - return true; - } - - return await WakeAndConnectWithRetries(powerOnRetries); - } - - internal async Task> GetAppsAsync(bool force) - { - var messages = new List - { - new RequestMessage("ms.channel.emit", new - { - data = "", - @event = "ed.edenApp.get", - to = "host", - }), + public enum PowerOffSource + { + Unknown, + App, + External + } + + public delegate void GenericDelegate(object sender); + + class SamsungDevice + { + public class InvokableAction + { + public Func, Task> AsyncFunction { get; set; } + public string Name { get; set; } + public Type EnumType { get; set; } + public decimal MinValue { get; set; } + public decimal MaxValue { get; set; } + public string Category { get; set; } + public string Title { get; set; } + public int CurrentValue { get; set; } + public int NumberOfValues { get; set; } + public bool Advanced { get; set; } + public SamsungPreset Preset { get; set; } + public List ValueLabels { get; set; } + } + + + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + public string Name { get; private set; } + public string IpAddress { get; private set; } + public string MacAddress { get; private set; } + public bool IsCustom { get; private set; } + public string Token { get; set; } + public SamsungDeviceOptions Options { get; set; } + + [JsonIgnore] + public bool IsDummy { get; private set; } + + [JsonIgnore] + public bool PoweredOn { get; private set; } + [JsonIgnore] + public DateTimeOffset PoweredOffAt { get; private set; } + [JsonIgnore] + public PowerOffSource PoweredOffBy { get; internal set; } + + public event GenericDelegate Connected; + + [JsonIgnore] + private SamTvConnection _samTvConnection; + + [JsonIgnore] + private ServiceManager _serviceManager; + [JsonIgnore] + private Timer _powerOffTimer; + [JsonIgnore] + private SemaphoreSlim _connectSemaphore = new SemaphoreSlim(1); + + [JsonIgnore] + private List _invokableActions = new List(); + + [JsonConstructor] + public SamsungDevice(string name, string ipAddress, string macAddress, bool isCustom = true, bool isDummy = false) + { + Options = new SamsungDeviceOptions(); + + Name = name; + IpAddress = ipAddress; + MacAddress = macAddress; + IsCustom = isCustom; + IsDummy = isDummy; + + AddInvokableAction("WOL", WakeAction); + AddInvokableAction("ServiceMenu", ServiceMenuAction, true); + AddInternalPresetAction(new SamsungPreset("PictureOff", null, new[] { "KEY_AD:1400", "KEY_DOWN:200", "KEY_DOWN:200", "KEY_ENTER" })); + AddInternalPresetAction(new SamsungPreset("PictureOn", null, new[] { "KEY_RETURN:1000", "KEY_RETURN", "KEY_RETURN" })); + + _serviceManager = Program.ServiceProvider.GetRequiredService(); + } + + public SamsungDevice(SamsungDeviceDto spec) : this(spec.Name, spec.IpAddress, spec.MacAddress, spec.IsCustom) + { + // TODO: call update method + if (spec.Options != null) + { + Options = spec.Options; + } + } + + ~SamsungDevice() + { + ClearPowerOffTask(); + } + + public bool Update(SamsungDeviceDto spec) + { + var reconnectNeeded = false; + + Name = spec.Name; + + if (IpAddress != spec.IpAddress) + { + reconnectNeeded = IsConnected(); + IpAddress = spec.IpAddress; + } + + MacAddress = spec.MacAddress; + IsCustom = spec.IsCustom; + + // TODO: call update method + if (spec.Options != null) + { + Options = spec.Options; + } + + return reconnectNeeded; + } + + public async Task ConnectAsync(bool force = false, bool retryToken = false) + { + if (!retryToken) + { + await _connectSemaphore.WaitAsync(); + } + try + { + if (force) + { + if (_samTvConnection != null) + { + await _samTvConnection.DisposeAsync(); + _samTvConnection = null; + } + } + else if (IsConnected()) + { + return true; + } + + _samTvConnection = new SamTvConnection(); + _samTvConnection.Connected += Connection_Connected; + var b64 = Utils.Base64Encode("ColorControl"); + + var firstTime = Token == null; + var readTimeout = firstTime ? 60000 : 5000; + + try + { + var protocol = Options.UseSecureConnection ? "wss" : "ws"; + var port = Options.UseSecureConnection ? 8002 : 8001; + + var uriString = $"{protocol}://{IpAddress}:{port}/api/v2/channels/samsung.remote.control?name={b64}"; + + if (Token != null) + { + uriString += $"&token={Token}"; + } + + var result = await _samTvConnection.Connect(new Uri(uriString), readTimeout); + + if (!result) + { + return false; + } + + if (firstTime && Token != null && !retryToken) + { + await _samTvConnection.DisposeAsync(); + _samTvConnection = null; + + return await ConnectAsync(force, true); + } + + return true; + } + catch (Exception ex) + { + Logger.Debug(ex); + + return false; + } + + } + finally + { + if (!retryToken) + { + _connectSemaphore.Release(); + } + + if (true) + { + Connected?.Invoke(this); + } + } + } + + private void Connection_Connected(string token, bool disconnect) + { + if (disconnect) + { + Logger.Debug($"Disconnect event, powered off by: {PoweredOffBy}"); + + if (_samTvConnection?.ClosedByDispose != true && PoweredOffBy == PowerOffSource.Unknown) + { + PoweredOffBy = PowerOffSource.External; + } + + return; + } + + Token = token; + + //_taskCompletionSource?.SetResult(token); + PoweredOffBy = PowerOffSource.Unknown; + } + + internal async Task PowerOffAsync() + { + try + { + await SendKeyAsync("KEY_POWER").WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(true); + } + catch (TimeoutException ex) + { + Logger.Debug($"Timeout when turning off tv: {ex.Message}"); + } + + PoweredOn = false; + PoweredOffAt = DateTimeOffset.Now; + PoweredOffBy = PowerOffSource.App; + } + + internal async Task WakeAndConnectWithRetries(int retries = 5) + { + var maxRetries = retries <= 1 ? 5 : retries; + + var result = false; + for (var retry = 0; retry < maxRetries && !result; retry++) + { + Logger.Debug($"WakeAndConnectWithRetries: attempt {retry + 1} of {maxRetries}..."); + + var ms = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + result = await WakeAndConnect(); + ms = DateTimeOffset.Now.ToUnixTimeMilliseconds() - ms; + if (!result) + { + var delay = 2000 - ms; + if (delay > 0) + { + await Task.Delay((int)delay); + } + } + } + + return result; + } + + internal async Task WakeAndConnect(int connectDelay = 1000) + { + try + { + var result = await Utils.PingHost(IpAddress); + + Logger.Debug($"Ping result: {result}"); + + var powerState = default(string); + + if (result) + { + powerState = await GetPowerStateAsync(); + } + + if (result && powerState == "on" && IsConnected()) + { + Logger.Debug("Already powered on and connected"); + return true; + } + + Logger.Debug($"Power state: {powerState}"); + + async Task SendWol() + { + if (!Wake()) + { + Logger.Debug("WOL failed"); + return false; + } + Logger.Debug("WOL succeeded"); + await Task.Delay(connectDelay); + + return true; + } + + if (!result || string.IsNullOrEmpty(powerState)) + { + await SendWol(); + } + + result = await ConnectAsync(true); + + if (powerState == "standby" || string.IsNullOrWhiteSpace(powerState)) + { + if (result) + { + Logger.Debug("Connected while standby"); + await SendKeyAsync("KEY_POWER"); + + await SendWol(); + //var powerState = await GetPowerStateAsync(); + + ////if (!PoweredOn && (PoweredOffAt == DateTimeOffset.MinValue || PoweredOffAt >= DateTimeOffset.Now.AddMinutes(-2))) + //if (powerState == "standby") + //{ + // await SendKeyAsync("KEY_POWER"); + //} + } + else + { + Logger.Debug("Initial connect while standby failed, send WOL"); + result = await SendWol(); + if (result) + { + result = await ConnectAsync(true); + Logger.Debug($"Connect after WOL result: {result}"); + } + } + } + + PoweredOn = true; + + Logger.Debug("Connect succeeded: " + result); + return result; + } + catch (Exception e) + { + Logger.Error("WakeAndConnectToSelectedDevice: " + e.ToLogString()); + return false; + } + } + + internal bool Wake() + { + var result = false; + + if (MacAddress != null) + { + var wolService = Program.ServiceProvider.GetRequiredService(); + + result = wolService.SendWol(MacAddress, IpAddress); + } + else + { + Logger.Debug("Cannot wake device: the device has no MAC-address"); + } + + return result; + } + + public async Task GetPowerStateAsync() + { + var url = $"http://{IpAddress}:8001/api/v2/"; + + var json = await Utils.GetRestJsonAsync(url); + + if (json == null) + { + Logger.Error("No power state received"); + + return string.Empty; + } + + var powerState = (string)json.device?.PowerState; + + Logger.Debug($"Current power state: {powerState}"); + + return powerState; + } + + internal async Task TestConnectionAsync() + { + var result = await Utils.PingHost(IpAddress); + + if (result) + { + return await ConnectAsync(); + } + + return false; + } + + public bool IsConnected() + { + return _samTvConnection?.ConnectionClosed == false; + } + + internal void ConvertToCustom() + { + IsCustom = true; + } + + public override string ToString() + { + return $"{(IsDummy ? string.Empty : (IsCustom ? "Custom: " : "Auto detect: "))}{Name}{(!string.IsNullOrEmpty(IpAddress) ? ", " + IpAddress : string.Empty)}"; + } + + internal async Task ExecutePresetAsync(SamsungPreset preset, Shared.Common.GlobalContext appContext = null, SamsungServiceConfig config = null) + { + if (config == null) + { + var service = Program.ServiceProvider.GetRequiredService(); + config = service.Config; + } + + var hasApp = !string.IsNullOrEmpty(preset.AppId); + + var hasWOL = preset.Steps.Any(s => s.Equals("WOL", StringComparison.OrdinalIgnoreCase)); + + if (hasWOL) + { + var connected = await WakeAndConnect(); + if (!connected) + { + return false; + } + } + + for (var tries = 0; tries <= 1; tries++) + { + if (!await ConnectAsync()) + { + return false; + } + + if (hasApp) + { + try + { + //await _lgTvApi.LaunchApp(preset.appId, @params); + } + catch (Exception ex) + { + string logMessage = ex.ToLogString(Environment.StackTrace); + Logger.Error("Error while launching app: " + logMessage); + + if (tries == 0) + { + continue; + } + return false; + } + + await Task.Delay(1000); + } + + if (preset.Steps.Any()) + { + if (hasApp) + { + await Task.Delay(1500); + } + try + { + await ExecuteStepsAsync(preset, appContext, config); + } + catch (Exception ex) + { + string logMessage = ex.ToLogString(Environment.StackTrace); + Logger.Error("Error while executing steps: " + logMessage); + + if (tries == 0) + { + continue; + } + return false; + } + } + + return true; + } + + return true; + } + + private async Task ExecuteStepsAsync(SamsungPreset preset, Shared.Common.GlobalContext appContext, SamsungServiceConfig config) + { + foreach (var step in preset.Steps) + { + var keySpec = step.Split(':'); + + var delay = config.DefaultButtonDelay; + var cmd = "Click"; + var key = step; + if (keySpec.Length >= 2) + { + delay = Utils.ParseInt(keySpec[1]); + if (delay > 0) + { + key = keySpec[0]; + } + } + if (keySpec.Length >= 3) + { + cmd = keySpec[2]; + } + + var index = key.IndexOf("("); + string[] parameters = null; + if (index > -1) + { + var keyValue = key.Split('('); + key = keyValue[0]; + parameters = keyValue[1].Substring(0, keyValue[1].Length - 1).Split(';'); + } + + var executeKey = true; + var action = _invokableActions.FirstOrDefault(a => a.Name.Equals(key, StringComparison.OrdinalIgnoreCase)); + if (action != null) + { + await ExecuteActionAsync(action, parameters, appContext, config); + + executeKey = false; + } + + if (parameters != null && await _serviceManager.HandleExternalServiceAsync(key, parameters)) + { + executeKey = false; + } + + if (executeKey) + { + await SendKeyAsync(key, cmd); + delay = delay == 0 ? config.DefaultButtonDelay : delay; + } + + if (delay > 0) + { + await Task.Delay(delay); + } + } + } + + public async Task ExecuteActionAsync(InvokableAction action, string[] parameters, Shared.Common.GlobalContext appContext, SamsungServiceConfig config) + { + if (action.Preset != null) + { + await ExecutePresetAsync(action.Preset, appContext, config); + + return; + } + if (action.AsyncFunction != null) + { + await action.AsyncFunction(null); + } + } + + private async Task SendKeyAsync(string key, string cmd = "Click") + { + var dynamic = new + { + Cmd = cmd, + DataOfCmd = key, + Option = "false", + TypeOfRemote = "SendRemoteKey" + }; + + var message = new RequestMessage("ms.remote.control", dynamic); + + await _samTvConnection.SendCommandAsync(message); + } + + internal void ClearPowerOffTask() + { + _powerOffTimer?.Dispose(); + } + + internal void PowerOffIn(int seconds) + { + ClearPowerOffTask(); + + _powerOffTimer = new Timer(PowerOffByTimer); + _powerOffTimer.Change(TimeSpan.FromSeconds(seconds), TimeSpan.FromSeconds(seconds)); + } + + private void PowerOffByTimer(object _) + { + ClearPowerOffTask(); + + var x = PerformActionOnScreenSaver(); + } + + private bool IsPowerOffAllowed() => true; + + public async Task PerformActionOnScreenSaver() + { + if (!IsPowerOffAllowed()) + { + return true; + } + + if (Options.TurnScreenOffOnScreenSaver) + { + Logger.Debug("Turning screen off on screen saver"); + await TurnPictureOff(); + + return true; + } + + Logger.Debug($"Device {Name} is now powering off due to delayed screensaver task"); + + await PowerOffAsync(); + + return true; + } + + private async Task TurnPictureOff() + { + var action = _invokableActions.FirstOrDefault(a => a.Name == "PictureOff"); + + if (action?.Preset != null) + { + await ExecutePresetAsync(action.Preset); + } + } + + private async Task TurnPictureOn() + { + var action = _invokableActions.FirstOrDefault(a => a.Name == "PictureOn"); + + if (action?.Preset != null) + { + await ExecutePresetAsync(action.Preset); + } + } + + public async Task PerformActionAfterScreenSaver(int powerOnRetries) + { + if (Options.TurnScreenOffOnScreenSaver && IsConnected()) + { + if (Options.TurnScreenOnAfterScreenSaver) + { + try + { + Logger.Debug("Turning screen on after screen saver"); + await TurnPictureOn(); + } + catch (Exception) { } + } + + return true; + } + + return await WakeAndConnectWithRetries(powerOnRetries); + } + + internal async Task> GetAppsAsync(bool force) + { + var messages = new List + { + new RequestMessage("ms.channel.emit", new + { + data = "", + @event = "ed.edenApp.get", + to = "host", + }), //new RequestMessage("ms.channel.emit", new //{ // data = "", @@ -672,194 +689,194 @@ namespace ColorControl.Services.Samsung //}) }; - foreach (var message in messages) - { - var result = await _samTvConnection.SendCommandAsync(message, true); - } + foreach (var message in messages) + { + var result = await _samTvConnection.SendCommandAsync(message); + } - return new List(); - } + return new List(); + } - private void AddInternalPresetAction(SamsungPreset preset) - { - var action = new InvokableAction - { - Name = preset.name, - Preset = preset - }; + private void AddInternalPresetAction(SamsungPreset preset) + { + var action = new InvokableAction + { + Name = preset.name, + Preset = preset + }; - _invokableActions.Add(action); - } + _invokableActions.Add(action); + } - private void AddInvokableAction(string name, Func, Task> function, bool advanced = false) - { - var action = new InvokableAction - { - Name = name, - AsyncFunction = function, - Category = "misc", - Title = name, - Advanced = advanced - }; + private void AddInvokableAction(string name, Func, Task> function, bool advanced = false) + { + var action = new InvokableAction + { + Name = name, + AsyncFunction = function, + Category = "misc", + Title = name, + Advanced = advanced + }; - _invokableActions.Add(action); - } + _invokableActions.Add(action); + } - internal IEnumerable GetInvokableActions(bool showAdvanced) - { - return _invokableActions.Where(a => showAdvanced || !a.Advanced); - } + internal IEnumerable GetInvokableActions(bool showAdvanced) + { + return _invokableActions.Where(a => showAdvanced || !a.Advanced); + } - private async Task WakeAction(Dictionary _) - { - return await WakeAndConnect(); - } + private async Task WakeAction(Dictionary _) + { + return await WakeAndConnect(); + } - private async Task ServiceMenuAction(Dictionary _) - { - var serviceMenuTypes = new[] { ServiceMenuType.FactoryMenu, ServiceMenuType.HospitalityMenu }.Select(t => Utils.GetDescription(t)); + private async Task ServiceMenuAction(Dictionary _) + { + var serviceMenuTypes = new[] { ServiceMenuType.FactoryMenu, ServiceMenuType.HospitalityMenu }.Select(t => Utils.GetDescription(t)); - var serviceMenuTypeField = new FieldDefinition - { - Label = "Service Menu Access Type", - SubLabel = "Both types will not reset your settings. 'FactoryMenu' is recommended for 2024 series but will also work for older ones. Click 'Next >' to continue or 'X' to stop", - FieldType = FieldType.DropDown, - Values = serviceMenuTypes, - Value = ServiceMenuType.FactoryMenu - }; + var serviceMenuTypeField = new FieldDefinition + { + Label = "Service Menu Access Type", + SubLabel = "Both types will not reset your settings. 'FactoryMenu' is recommended for 2024 series but will also work for older ones. Click 'Next >' to continue or 'X' to stop", + FieldType = FieldType.DropDown, + Values = serviceMenuTypes, + Value = ServiceMenuType.FactoryMenu + }; - if (!MessageForms.ShowDialog($"Access Service Menu", [serviceMenuTypeField], okButtonText: "Next >").Any()) - { - return true; - } + if (!MessageForms.ShowDialog($"Access Service Menu", [serviceMenuTypeField], okButtonText: "Next >").Any()) + { + return true; + } - return serviceMenuTypeField.ValueAsEnum() switch - { - ServiceMenuType.FactoryMenu => await OpenFactoryMenu(), - ServiceMenuType.HospitalityMenu => await OpenHospitalityMenu(), - _ => throw new NotImplementedException() - }; - } + return serviceMenuTypeField.ValueAsEnum() switch + { + ServiceMenuType.FactoryMenu => await OpenFactoryMenu(), + ServiceMenuType.HospitalityMenu => await OpenHospitalityMenu(), + _ => throw new NotImplementedException() + }; + } - private async Task OpenHospitalityMenu() - { - await ExecutePresetWithProgressAsync(SamsungHospitalityMenuPresets.Preset1, "Step 1: opening Service Menu. It is normal to see messages like 'Not Available'."); + private async Task OpenHospitalityMenu() + { + await ExecutePresetWithProgressAsync(SamsungHospitalityMenuPresets.Preset1, "Step 1: opening Service Menu. It is normal to see messages like 'Not Available'."); - if (!MessageForms.ShowDialog($"Access Service Menu - Step 2", [ - new FieldDefinition - { - Label = "Service Menu opened?", - SubLabel = "If the menu is opened with the 'Hospitality Mode' item highlighted and the rest disabled, click 'Next >' to continue or 'X' to stop", - FieldType = FieldType.Label - } ], okButtonText: "Next >").Any()) - { - return true; - } + if (!MessageForms.ShowDialog($"Access Service Menu - Step 2", [ + new FieldDefinition + { + Label = "Service Menu opened?", + SubLabel = "If the menu is opened with the 'Hospitality Mode' item highlighted and the rest disabled, click 'Next >' to continue or 'X' to stop", + FieldType = FieldType.Label + } ], okButtonText: "Next >").Any()) + { + return true; + } - await ExecutePresetWithProgressAsync(SamsungHospitalityMenuPresets.Preset2, "Step 2: accessing Service Menu..."); + await ExecutePresetWithProgressAsync(SamsungHospitalityMenuPresets.Preset2, "Step 2: accessing Service Menu..."); - var values = MessageForms.ShowDialog($"Access Service Menu - Step 3", new[] { - new FieldDefinition - { - Label = "Service Menu - Advanced", - SubLabel = "If the 'Advanced' item is highlighted and you want to access these settings, click 'Next >' or click 'X' to skip", - FieldType = FieldType.Label - } }, okButtonText: "Next >"); + var values = MessageForms.ShowDialog($"Access Service Menu - Step 3", new[] { + new FieldDefinition + { + Label = "Service Menu - Advanced", + SubLabel = "If the 'Advanced' item is highlighted and you want to access these settings, click 'Next >' or click 'X' to skip", + FieldType = FieldType.Label + } }, okButtonText: "Next >"); - if (values.Any()) - { - await ExecutePresetWithProgressAsync(SamsungHospitalityMenuPresets.Preset3, "Step 3: accessing Advanced Service Menu..."); - } + if (values.Any()) + { + await ExecutePresetWithProgressAsync(SamsungHospitalityMenuPresets.Preset3, "Step 3: accessing Advanced Service Menu..."); + } - if (!MessageForms.ShowDialog($"Reboot TV - Final Step", new[] { - new FieldDefinition - { - Label = "Reboot TV", - SubLabel = "After you have made the necessary changes, click 'Reboot TV' to reboot the TV or click 'X' to close. If the TV turns not back on automatically, power it on manually.", - FieldType = FieldType.Label - } }, okButtonText: "Reboot TV").Any()) - { - return true; - } + if (!MessageForms.ShowDialog($"Reboot TV - Final Step", new[] { + new FieldDefinition + { + Label = "Reboot TV", + SubLabel = "After you have made the necessary changes, click 'Reboot TV' to reboot the TV or click 'X' to close. If the TV turns not back on automatically, power it on manually.", + FieldType = FieldType.Label + } }, okButtonText: "Reboot TV").Any()) + { + return true; + } - await ExecutePresetWithProgressAsync(SamsungGenericPresets.Reboot, "Final Step: rebooting TV..."); + await ExecutePresetWithProgressAsync(SamsungGenericPresets.Reboot, "Final Step: rebooting TV..."); - return true; - } + return true; + } - private async Task OpenFactoryMenu() - { - await ExecutePresetWithProgressAsync(SamsungFactoryMenuPresets.Preset1, "Step 1: opening Service Menu. It is normal to see messages like 'Not Available'."); + private async Task OpenFactoryMenu() + { + await ExecutePresetWithProgressAsync(SamsungFactoryMenuPresets.Preset1, "Step 1: opening Service Menu. It is normal to see messages like 'Not Available'."); - var values = MessageForms.ShowDialog($"Access Service Menu - Step 2", new[] { - new FieldDefinition - { - Label = "Service Menu - Advanced", - SubLabel = "If the service menu opened and you want to access the Advanced settings, click 'Next >' or click 'X' to skip", - FieldType = FieldType.Label - } }, okButtonText: "Next >"); + var values = MessageForms.ShowDialog($"Access Service Menu - Step 2", new[] { + new FieldDefinition + { + Label = "Service Menu - Advanced", + SubLabel = "If the service menu opened and you want to access the Advanced settings, click 'Next >' or click 'X' to skip", + FieldType = FieldType.Label + } }, okButtonText: "Next >"); - if (values.Any()) - { - await ExecutePresetWithProgressAsync(SamsungFactoryMenuPresets.PresetAdvanced, "Step 2: accessing Advanced Service Menu..."); - } + if (values.Any()) + { + await ExecutePresetWithProgressAsync(SamsungFactoryMenuPresets.PresetAdvanced, "Step 2: accessing Advanced Service Menu..."); + } - var exitServiceMenuTypes = new[] { ExitServiceMenuType.Exit, ExitServiceMenuType.Reboot }.Select(t => Utils.GetDescription(t)); + var exitServiceMenuTypes = new[] { ExitServiceMenuType.Exit, ExitServiceMenuType.Reboot }.Select(t => Utils.GetDescription(t)); - var exitServiceMenuTypeField = new FieldDefinition - { - Label = "Exit Service Menu Type", - SubLabel = "Choose how to exit the service menu. You can either reboot the TV or just exit the service menu (if, for example, no changes have been made)", - FieldType = FieldType.DropDown, - Values = exitServiceMenuTypes, - Value = ExitServiceMenuType.Exit - }; + var exitServiceMenuTypeField = new FieldDefinition + { + Label = "Exit Service Menu Type", + SubLabel = "Choose how to exit the service menu. You can either reboot the TV or just exit the service menu (if, for example, no changes have been made)", + FieldType = FieldType.DropDown, + Values = exitServiceMenuTypes, + Value = ExitServiceMenuType.Exit + }; - if (!MessageForms.ShowDialog($"Exit Service Menu", [exitServiceMenuTypeField], okButtonText: "Next >").Any()) - { - return true; - } + if (!MessageForms.ShowDialog($"Exit Service Menu", [exitServiceMenuTypeField], okButtonText: "Next >").Any()) + { + return true; + } - var task = exitServiceMenuTypeField.ValueAsEnum() switch - { - ExitServiceMenuType.Exit => ExecutePresetWithProgressAsync(SamsungFactoryMenuPresets.Exit, "Final Step: exiting service menu..."), - ExitServiceMenuType.Reboot => ExecutePresetWithProgressAsync(SamsungGenericPresets.Reboot, "Final Step: rebooting TV..."), - _ => throw new NotImplementedException() - }; + var task = exitServiceMenuTypeField.ValueAsEnum() switch + { + ExitServiceMenuType.Exit => ExecutePresetWithProgressAsync(SamsungFactoryMenuPresets.Exit, "Final Step: exiting service menu..."), + ExitServiceMenuType.Reboot => ExecutePresetWithProgressAsync(SamsungGenericPresets.Reboot, "Final Step: rebooting TV..."), + _ => throw new NotImplementedException() + }; - await task; + await task; - return true; - } + return true; + } - private async Task ExecutePresetWithProgressAsync(SamsungPreset preset, string text) - { - var progressForm = MessageForms.ShowProgress(text); - try - { - //await Task.Delay(5000); - await ExecutePresetAsync(preset); - } - finally - { - progressForm.Close(); - } - } + private async Task ExecutePresetWithProgressAsync(SamsungPreset preset, string text) + { + var progressForm = MessageForms.ShowProgress(text); + try + { + //await Task.Delay(5000); + await ExecutePresetAsync(preset); + } + finally + { + progressForm.Close(); + } + } - internal async Task ExecuteInvokableAction(InvokableActionDto invokableAction, GlobalContext appContext, SamsungServiceConfig config) - { - var action = _invokableActions.FirstOrDefault(a => a.Name == invokableAction.Name); + internal async Task ExecuteInvokableAction(InvokableActionDto invokableAction, GlobalContext appContext, SamsungServiceConfig config) + { + var action = _invokableActions.FirstOrDefault(a => a.Name == invokableAction.Name); - if (action == null) - { - return false; - } + if (action == null) + { + return false; + } - await ExecuteActionAsync(action, [], appContext, config); + await ExecuteActionAsync(action, [], appContext, config); - return true; - } - } + return true; + } + } } diff --git a/ColorControl/Services/Samsung/SamsungService.cs b/ColorControl/Services/Samsung/SamsungService.cs index e46c335..cab6a83 100644 --- a/ColorControl/Services/Samsung/SamsungService.cs +++ b/ColorControl/Services/Samsung/SamsungService.cs @@ -19,951 +19,952 @@ using System.Threading.Tasks; namespace ColorControl.Services.Samsung { - class SamsungService : ServiceBase, ISamsungService - { - private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - - public override string ServiceName => "Samsung"; - - protected override string PresetsBaseFilename => "SamsungPresets.json"; - - public SamsungServiceConfig Config { get; private set; } - public SamsungDevice SelectedDevice { get; set; } - - private bool _allowPowerOn; - private ServiceManager _serviceManager; - private readonly PowerEventDispatcher _powerEventDispatcher; - private readonly DisplayChangeEventDispatcher _displayChangeEventDispatcher; - private readonly SessionSwitchDispatcher _sessionSwitchDispatcher; - private readonly RestartDetector _restartDetector; - private readonly ProcessEventDispatcher _processEventDispatcher; - private readonly WinApiAdminService _winApiAdminService; - private string _configFilename; - private bool _poweredOffByScreenSaver; - private int _poweredOffByScreenSaverProcessId; - private List _samsungApps = new List(); - private Shared.Common.GlobalContext _appContext; - - public List Devices { get; private set; } - - public static readonly int SHORTCUTID_SAMSUNGQA = -204; - - public SamsungService( - GlobalContext globalContext, - ServiceManager serviceManager, - PowerEventDispatcher powerEventDispatcher, - DisplayChangeEventDispatcher displayChangeEventDispatcher, - SessionSwitchDispatcher sessionSwitchDispatcher, - RestartDetector restartDetector, - ProcessEventDispatcher processEventDispatcher, - WinApiAdminService winApiAdminService) : base(globalContext) - { - _appContext = globalContext; - _allowPowerOn = _appContext.StartUpParams.RunningFromScheduledTask; - _serviceManager = serviceManager; - _powerEventDispatcher = powerEventDispatcher; - _displayChangeEventDispatcher = displayChangeEventDispatcher; - _sessionSwitchDispatcher = sessionSwitchDispatcher; - _restartDetector = restartDetector; - _processEventDispatcher = processEventDispatcher; - _winApiAdminService = winApiAdminService; - SamTvConnection.SyncContext = _appContext.SynchronizationContext; - SamsungPreset.GetDeviceName += SamsungPreset_GetDeviceName; - - LoadConfig(); - LoadPresets(); - } - - private string SamsungPreset_GetDeviceName(string macAddress) - { - var device = Devices?.FirstOrDefault(d => !string.IsNullOrEmpty(d.MacAddress) && d.MacAddress.Equals(macAddress)); - if (device != null) - { - return device.Name; - } - - return "Unknown: device not found"; - } - - public static async Task ExecutePresetAsync(string presetName) - { - try - { - var samsungService = Program.ServiceProvider.GetRequiredService(); - - await samsungService.RefreshDevices(afterStartUp: true); - - var result = await samsungService.ApplyPreset(presetName); - - if (!result) - { - Console.WriteLine("Preset not found or error while executing."); - } - - return result; - } - catch (Exception ex) - { - Console.WriteLine("Error executing preset: " + ex.ToLogString()); - return false; - } - } - - public override void InstallEventHandlers() - { - base.InstallEventHandlers(); - - SetShortcuts(SHORTCUTID_SAMSUNGQA, Config.QuickAccessShortcut); - - _powerEventDispatcher.RegisterAsyncEventHandler(PowerEventDispatcher.Event_Startup, PowerStateChanged); - _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_MonitorOn, PowerModeChanged); - _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_MonitorOff, PowerModeChanged); - _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_Suspend, PowerModeChanged); - _powerEventDispatcher.RegisterAsyncEventHandler(PowerEventDispatcher.Event_Resume, PowerModeResume); - _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_Shutdown, PowerModeChanged); - _processEventDispatcher.RegisterAsyncEventHandler(ProcessEventDispatcher.Event_ProcessChanged, ProcessChanged); - _displayChangeEventDispatcher.RegisterAsyncEventHandler(DisplayChangeEventDispatcher.Event_DisplayChanged, DisplayChanged); - _sessionSwitchDispatcher.RegisterAsyncEventHandler(SessionSwitchDispatcher.Event_SessionSwitch, SessionSwitched); - } - - public override List GetInfo() - { - return [$"{_presets.Count} presets", $"{Devices?.Count ?? 0} devices"]; - } - - private async Task DisplayChanged(object sender, DisplayChangedEventArgs e, CancellationToken ct) - { - await ExecutePresetsForEvent(PresetTriggerType.DisplayChange); - } - - private async Task PowerStateChanged(object sender, PowerStateChangedEventArgs e, CancellationToken token) - { - await RefreshDevices(afterStartUp: true); - - if (_appContext.StartUpParams.RunningFromScheduledTask) - { - await ExecutePresetsForEvent(PresetTriggerType.Startup); - } - - if (_appContext.StartUpParams.ExecuteSamsungPreset) - { - await ExecutePresetAsync(_appContext.StartUpParams.SamsungPresetName); - } - } - - private async Task SessionSwitched(object sender, SessionSwitchEventArgs e, CancellationToken token) - { - if (e.Reason == SessionSwitchReason.SessionLock) - { - var devices = Devices.Where(d => d.Options.PowerOffAfterSessionLock); - PowerOffDevices(devices, PowerOnOffState.StandBy); - } - else if (e.Reason == SessionSwitchReason.SessionUnlock) - { - var devices = Devices.Where(d => d.Options.PowerOnAfterSessionUnlock).ToList(); - await PowerOnDevicesTask(devices); - } - } - - private void LoadConfig() - { - _configFilename = Path.Combine(_dataPath, "SamsungConfig.json"); - try - { - if (File.Exists(_configFilename)) - { - Config = JsonConvert.DeserializeObject(File.ReadAllText(_configFilename)); - } - } - catch (Exception ex) - { - Logger.Error($"LoadConfig: {ex.Message}"); - } - Config ??= new SamsungServiceConfig(); - } - - private async Task PowerModeResume(object sender, PowerStateChangedEventArgs e, CancellationToken _) - { - Logger.Debug($"PowerModeChanged: {e.State}"); - - await WakeAfterStartupOrResume(PowerOnOffState.Resume); - - await ExecutePresetsForEvent(PresetTriggerType.Resume); - } - - private void PowerModeChanged(object sender, PowerStateChangedEventArgs e) - { - Logger.Debug($"PowerModeChanged: {e.State}"); - - if (Devices?.Any() != true) - { - Logger.Debug("Devices have not been loaded, ignoring event"); - return; - } - - switch (e.State) - { - case PowerOnOffState.StandBy: - { - //_ = ExecutePresetsForEvent(PresetTriggerType.Standby).ConfigureAwait(true); - - var devices = Devices.Where(d => d.Options.PowerOffOnStandby); - PowerOffDevices(devices, PowerOnOffState.StandBy); - break; - } - case PowerOnOffState.ShutDown: - { - _ = ExecutePresetsForEvent(PresetTriggerType.Shutdown).ConfigureAwait(true); - - var devices = Devices.Where(d => d.Options.PowerOffOnShutdown); - - PowerOffDevices(devices); - break; - } - - case PowerOnOffState.MonitorOff: - { - var devices = Devices.Where(d => d.Options.PowerOffByWindows).ToList(); - PowerOffDevices(devices, PowerOnOffState.MonitorOff); - break; - } - case PowerOnOffState.MonitorOn: - { - var devices = Devices.Where(d => d.Options.PowerOnByWindows).ToList(); - PowerOnDevicesTask(devices); - break; - } - } - } - - private async Task ExecutePresetsForEvent(PresetTriggerType triggerType) - { - Logger.Debug($"Executing presets for event {triggerType}"); - - var presets = _presets.Where(p => p.Triggers.Any(t => t.Trigger == triggerType)).ToList(); - - if (!presets.Any()) - { - return; - } - - var applicableDevices = Devices.Where(d => d.Options.TriggersEnabled && - presets.Any(p => (string.IsNullOrEmpty(p.DeviceMacAddress) && d == SelectedDevice) || p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase))).ToList(); - - if (!applicableDevices.Any()) - { - return; - } - - await ExecuteEventPresets(_serviceManager, new[] { triggerType }).ConfigureAwait(true); - } - - public async Task RefreshDevices(bool connect = true, bool afterStartUp = false) - { - Devices = Config.Devices; - - if (!_globalContext.StartUpParams.NoDeviceRefresh) - { - var customIpAddresses = Devices.Where(d => d.IsCustom).Select(d => d.IpAddress); - - var pnpDevices = await Utils.GetPnpDevices(Config.DeviceSearchKey, Utils.DEV_CLASS_DISPLAY_TV_LCD); - - var autoDevices = pnpDevices.Where(p => !customIpAddresses.Contains(p.IpAddress)).Select(d => new SamsungDevice(d.Name, d.IpAddress, d.MacAddress, false)).ToList(); - var autoIpAddresses = pnpDevices.Select(d => d.IpAddress); - - Devices.RemoveAll(d => !d.IsCustom && !autoIpAddresses.Contains(d.IpAddress)); - - var newAutoDevices = autoDevices.Where(ad => ad.IpAddress != null && !Devices.Any(d => d.IpAddress != null && d.IpAddress.Equals(ad.IpAddress))); - Devices.AddRange(newAutoDevices); - } - - if (Devices.Any()) - { - var preferredDevice = Devices.FirstOrDefault(x => x.MacAddress != null && x.MacAddress.Equals(Config.PreferredMacAddress)) ?? Devices[0]; - - SelectedDevice = preferredDevice; - - SelectedDevice.Connected += SelectedDevice_Connected; - } - else - { - SelectedDevice = null; - } - - if (afterStartUp && SelectedDevice == null && Config.PowerOnAfterStartup && !string.IsNullOrEmpty(Config.PreferredMacAddress)) - { - Logger.Debug("No device has been found, trying to wake it first..."); - - var tempDevice = new SamsungDevice("Test", string.Empty, Config.PreferredMacAddress); - tempDevice.Wake(); - - await Task.Delay(4000); - return await RefreshDevices(); - } - - if (connect && SelectedDevice != null) - { - if (_allowPowerOn) - { - await WakeAfterStartupOrResume(); - } - else - { - var _ = SelectedDevice.ConnectAsync(); - } - } - - return true; - } - - private void SelectedDevice_Connected(object sender) - { - if (_samsungApps.Any()) - { - return; - } - - //await RefreshAppsAsync(); - } - - protected override List GetDefaultPresets() - { - return new List(); - } - - private void SaveConfig() - { - Config.PowerOnAfterStartup = Devices?.Any(d => d.Options.PowerOnAfterStartup) ?? false; - - Utils.WriteObject(_configFilename, Config); - } - - public void GlobalSave() - { - SavePresets(); - SaveConfig(); - } - - public SamsungDevice GetPresetDevice(SamsungPreset preset) - { - var device = string.IsNullOrEmpty(preset.DeviceMacAddress) - ? SelectedDevice - : Devices.FirstOrDefault(d => d.MacAddress?.Equals(preset.DeviceMacAddress, StringComparison.OrdinalIgnoreCase) ?? false); - return device; - } - - public override async Task ApplyPreset(SamsungPreset preset) - { - return await ApplyPreset(preset, _appContext); - } - - private async Task ApplyPreset(SamsungPreset preset, Shared.Common.GlobalContext appContext) - { - var result = true; - - var device = GetPresetDevice(preset); - - if (device == null) - { - return false; - } - - await device.ExecutePresetAsync(preset, appContext, Config); - - _lastAppliedPreset = preset; - - PresetApplied(); - - return result; - } - - internal void AddCustomDevice(SamsungDevice device) - { - Devices.Add(device); - } - - internal void RemoveCustomDevice(SamsungDevice device) - { - Devices.Remove(device); - - if (!string.IsNullOrEmpty(device.MacAddress)) - { - var presets = _presets.Where(p => !string.IsNullOrEmpty(p.DeviceMacAddress) && p.DeviceMacAddress.Equals(device.MacAddress, StringComparison.OrdinalIgnoreCase)).ToList(); - presets.ForEach(preset => - { - preset.DeviceMacAddress = null; - }); - } - - if (SelectedDevice == device) - { - SelectedDevice = Devices.Any() ? Devices.First() : null; - } - } - - private Task WakeAfterStartupOrResume(PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) - { - Devices.ForEach(d => d.ClearPowerOffTask()); - - var wakeDevices = Devices.Where(d => state == PowerOnOffState.StartUp && d.Options.PowerOnAfterStartup || - state == PowerOnOffState.Resume && d.Options.PowerOnAfterResume || - state == PowerOnOffState.ScreenSaver && d.Options.PowerOnAfterScreenSaver); - - return PowerOnDevicesTask(wakeDevices, state, checkUserSession); - } - - private void PowerOffDevices(IEnumerable devices, PowerOnOffState state = PowerOnOffState.ShutDown) - { - if (!devices.Any()) - { - return; - } - - if (NativeMethods.GetAsyncKeyState(NativeConstants.VK_CONTROL) < 0 || NativeMethods.GetAsyncKeyState(NativeConstants.VK_RCONTROL) < 0) - { - Logger.Debug("Not powering off because CONTROL-key is down"); - return; - } - - if (state == PowerOnOffState.ShutDown) - { - var sleep = Config.ShutdownDelay; - - Logger.Debug($"PowerOffDevices: Waiting for a maximum of {sleep} milliseconds..."); - - while (sleep > 0 && _restartDetector?.PowerOffDetected == false) - { - Thread.Sleep(100); - - if (_restartDetector != null && (_restartDetector.RestartDetected || _restartDetector.IsRebootInProgress())) - { - Logger.Debug("Not powering off because of a restart"); - return; - } - - sleep -= 100; - } - } - - Logger.Debug("Powering off tv(s)..."); - - var tasks = new List(); - foreach (var device in devices) - { - var task = device.PowerOffAsync(); - - tasks.Add(task); - } - - if (state == PowerOnOffState.StandBy) - { - var task = Task.Run(() => - { - var standByScript = Path.Combine(Program.DataDir, "StandByScript.bat"); - if (File.Exists(standByScript)) - { - _winApiAdminService.StartProcess(standByScript, hidden: true, wait: true, useShellExecute: false); - } - }); - - tasks.Add(task); - } - - // We can't use async here because we need to stay on the main thread... - var i = 0; - while (tasks.Any(t => t.Status < TaskStatus.WaitingForChildrenToComplete) && i < 200) - { - Thread.Sleep(10); - System.Windows.Forms.Application.DoEvents(); - i++; - } - - //var _ = Task.WhenAll(tasks.ToArray()); - - Logger.Debug("Done powering off tv(s)"); - } - - private Task PowerOnDevicesTask(IEnumerable devices, PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) - { - return Task.Run(async () => await PowerOnDevices(devices, state, checkUserSession)); - } - - private async Task PowerOnDevices(IEnumerable devices, PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) - { - if (!devices.Any()) - { - return; - } - - if (checkUserSession && !_sessionSwitchDispatcher.UserLocalSession) - { - Logger.Debug($"WakeAfterStartupOrResume: not waking because session info indicates no local session"); - return; - } - - var resumeScript = Path.Combine(Program.DataDir, "ResumeScript.bat"); - - var tasks = new List(); - - foreach (var device in devices) - { - if (!device.Options.PowerOnAfterManualPowerOff && device.PoweredOffBy == PowerOffSource.External) - { - Logger.Debug($"[{device.Name}]: device was manually powered off by user, not powering on"); - continue; - } - - if (state == PowerOnOffState.ScreenSaver) - { - tasks.Add(device.PerformActionAfterScreenSaver(Config.PowerOnRetries)); - continue; - } - - var task = device.WakeAndConnectWithRetries(Config.PowerOnRetries); - tasks.Add(task); - } - - await Task.WhenAll(tasks.ToArray()); - - if (File.Exists(resumeScript)) - { - _winApiAdminService.StartProcess(resumeScript, hidden: true); - } - } - - private async Task RefreshAppsAsync(bool force = false) - { - if (SelectedDevice == null) - { - Logger.Debug("Cannot refresh apps: no device has been selected"); - return; - } - - var apps = await SelectedDevice.GetAppsAsync(force); - - if (apps.Any()) - { - _samsungApps.Clear(); - _samsungApps.AddRange(apps); - } - } - - internal List GetApps() - { - return _samsungApps; - } - - private async Task ProcessChanged(object sender, ProcessChangedEventArgs args, CancellationToken token) - { - try - { - var wasConnected = Devices?.Any(d => d.IsConnected()); - - if (!wasConnected.HasValue) - { - return; - } - - var applicableDevices = Devices.Where(d => - d.Options.PowerOffOnScreenSaver - || d.Options.PowerOnAfterScreenSaver - || (d.Options.TriggersEnabled - // only these trigger types are ever triggered by a process changed event - && _presets.Any(p => p.Triggers.Any(t => t.Trigger == PresetTriggerType.ProcessSwitch || t.Trigger == PresetTriggerType.ScreensaverStart || t.Trigger == PresetTriggerType.ScreensaverStop) - && ((string.IsNullOrEmpty(p.DeviceMacAddress) && d == SelectedDevice) || p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase))))) - .ToList(); - - if (!applicableDevices.Any()) - { - return; - } - - if (_poweredOffByScreenSaver && !_sessionSwitchDispatcher.UserLocalSession) - { - return; - } - - var processes = args.RunningProcesses; - - if (_poweredOffByScreenSaver) - { - if (processes.Any(p => p.Id == _poweredOffByScreenSaverProcessId)) - { - return; - } - - var process = processes.FirstOrDefault(p => p.ProcessName.ToLowerInvariant().EndsWith(".scr")); - - if (process != null) - { - Logger.Debug($"Detected screen saver process switch from {_poweredOffByScreenSaverProcessId} to {process.Id}"); - - _poweredOffByScreenSaverProcessId = process.Id; - return; - } - - Logger.Debug("Screensaver stopped, waking"); - _poweredOffByScreenSaver = false; - _poweredOffByScreenSaverProcessId = 0; - - await WakeAfterStartupOrResume(PowerOnOffState.ScreenSaver); - - await ExecuteScreenSaverPresetsCustom(args, applicableDevices); - - return; - } - - var connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); - - if (!connectedDevices.Any()) - { - if (wasConnected == true) - { - Logger.Debug("Process monitor: TV(s) where connected, but not any longer"); - wasConnected = false; - - return; - } - else - { - var devices = applicableDevices.ToList(); - foreach (var device in devices) - { - //Logger.Debug($"Process monitor: test connection with {device.Name}..."); - - var test = await device.TestConnectionAsync(); - //Logger.Debug("Process monitor: test connection result: " + test); - } - - connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); - - if (!connectedDevices.Any()) - { - return; - } - } - } - - if (wasConnected == false) - { - Logger.Debug("Process monitor: TV(s) where not connected, but connection has now been established"); - wasConnected = true; - } - - if (connectedDevices.Any(d => d.Options.PowerOffOnScreenSaver || d.Options.PowerOnAfterScreenSaver)) - { - await HandleScreenSaverProcessAsync(processes, connectedDevices); - } - - await ExecuteProcessPresets(args, connectedDevices); - - await ExecuteScreenSaverPresetsCustom(args, connectedDevices); - } - catch (Exception ex) - { - Logger.Error("ProcessChanged: " + ex.ToLogString()); - } - } - - private async Task ExecuteScreenSaverPresetsCustom(ProcessChangedEventArgs context, IList connectedDevices) - { - var triggerDevices = connectedDevices.Where(d => d.Options.TriggersEnabled).ToList(); - - if (!triggerDevices.Any()) - { - return; - } - - await ExecuteScreenSaverPresets(_serviceManager, context); - } - - private async Task ExecuteProcessPresets(ProcessChangedEventArgs context, IList connectedDevices) - { - var triggerDevices = connectedDevices.Where(d => d.Options.TriggersEnabled).ToList(); - - if (context.IsScreenSaverActive) - { - return; - } - - var selectedDevice = SelectedDevice; - - var presets = _presets.Where(p => p.Triggers.Any(t => t.Trigger == PresetTriggerType.ProcessSwitch) && - ((string.IsNullOrEmpty(p.DeviceMacAddress) && selectedDevice?.Options.TriggersEnabled == true) - || Devices.Any(d => d.Options.TriggersEnabled && p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase)))); - if (!presets.Any()) - { - return; - } - - var changedProcesses = new List(); - if (context.ForegroundProcess != null) - { - changedProcesses.Add(context.ForegroundProcess); - } - - if (context.ForegroundProcess != null && context.ForegroundProcessIsFullScreen) - { - presets = presets.Where(p => p.Triggers.Any(t => t.Conditions.HasFlag(PresetConditionType.FullScreen))); - } - else if (context.StoppedFullScreen) - { - presets = presets.Where(p => p.Triggers.Any(t => !t.Conditions.HasFlag(PresetConditionType.FullScreen))); - } - - var triggerContext = await CreateTriggerContext(_serviceManager, context); - - var toApplyPresets = presets.Where(p => p.Triggers.Any(t => t.TriggerActive(triggerContext))).ToList(); - - if (toApplyPresets.Any()) - { - var toApplyPreset = toApplyPresets.FirstOrDefault(p => toApplyPresets.Count == 1 || p.Triggers.Any(t => !t.IncludedProcesses.Contains("*"))) ?? toApplyPresets.First(); - - if (_lastTriggeredPreset != toApplyPreset) - { - Logger.Debug($"Applying triggered preset: {toApplyPreset.name} because trigger conditions are met"); - - await ApplyPreset(toApplyPreset); - - _lastTriggeredPreset = toApplyPreset; - } - } - } - - private async Task HandleScreenSaverProcessAsync(IList processes, List connectedDevices) - { - var process = processes.FirstOrDefault(p => p.ProcessName.ToLowerInvariant().EndsWith(".scr")); - - var powerOffDevices = connectedDevices.Where(d => d.Options.PowerOffOnScreenSaver).ToList(); - - if (process == null) - { - powerOffDevices.ForEach(d => d.ClearPowerOffTask()); - - return; - } - - var parent = process.Parent(processes); - - // Ignore manually started screen savers if no devices prefer this - if (parent?.ProcessName.Contains("winlogon") != true && !connectedDevices.Any(d => d.Options.HandleManualScreenSaver)) - { - return; - } - - Logger.Debug($"Screensaver started: {process.ProcessName}, parent: {parent.ProcessName}"); - try - { - foreach (var device in powerOffDevices) - { - Logger.Debug($"Screensaver check: test connection with {device.Name}..."); - - var test = await device.TestConnectionAsync(); - Logger.Debug("Screensaver check: test connection result: " + test); - if (!test) - { - continue; - } - - if (device.Options.ScreenSaverMinimalDuration > 0) - { - Logger.Debug($"Screensaver check: powering off tv {device.Name} in {device.Options.ScreenSaverMinimalDuration} seconds because of screensaver"); - - _poweredOffByScreenSaver = true; - - device.PowerOffIn(device.Options.ScreenSaverMinimalDuration); - - continue; - } - - Logger.Debug($"Screensaver check: powering off tv {device.Name} because of screensaver"); - try - { - _poweredOffByScreenSaver = await device.PerformActionOnScreenSaver().WaitAsync(TimeSpan.FromSeconds(5)); - } - catch (TimeoutException) - { - // Assume powering off was successful - _poweredOffByScreenSaver = true; - Logger.Error($"Screensaver check: timeout exception while powering off (probably connection closed)"); - } - catch (Exception pex) - { - // Assume powering off was successful - _poweredOffByScreenSaver = true; - Logger.Error($"Screensaver check: exception while powering off (probably connection closed): " + pex.Message); - } - Logger.Debug($"Screensaver check: powered off tv {device.Name}"); - } - - _poweredOffByScreenSaverProcessId = process.Id; - } - catch (Exception e) - { - Logger.Error($"Screensaver check: can't power off: " + e.ToLogString()); - } - } - - public async Task ExecuteEventPresets(PresetTriggerType triggerType) - { - await ExecuteEventPresets(_serviceManager, new[] { triggerType }); - } - - public List GetDevices() - { - return Devices?.Select(d => new SamsungDeviceDto - { - Name = d.Name, - MacAddress = d.MacAddress, - IpAddress = d.IpAddress, - IsCustom = d.IsCustom, - IsDummy = d.IsDummy, - Token = d.Token, - Options = d.Options, - IsConnected = d.IsConnected(), - IsSelected = d == SelectedDevice - }).ToList(); - } - - public async Task UpdateDevice(SamsungDeviceDto deviceSpec) - { - if (Devices == null) - { - return false; - } - - var device = Devices.FirstOrDefault(d => d.MacAddress == deviceSpec.MacAddress); - - if (device == null) - { - device = new SamsungDevice(deviceSpec); - - Devices.Add(device); - } - else - { - var reconnect = device.Update(deviceSpec); - - if (reconnect) - { - await device.ConnectAsync(true); - } - } - - SaveConfig(); - - return true; - } - - public async Task TestDevice(SamsungDeviceDto deviceSpec) - { - if (Devices == null) - { - return false; - } - - try - { - var device = new SamsungDevice(deviceSpec); - - return await device.TestConnectionAsync(); - } - catch - { - return false; - } - } - - public SamsungServiceConfigDto GetConfig() - { - return new SamsungServiceConfigDto - { - DefaultButtonDelay = Config.DefaultButtonDelay, - DeviceSearchKey = Config.DeviceSearchKey, - Devices = GetDevices(), - PowerOnAfterStartup = Config.PowerOnAfterStartup, - PowerOnRetries = Config.PowerOnRetries, - PreferredMacAddress = Config.PreferredMacAddress, - QuickAccessShortcut = Config.QuickAccessShortcut, - SetSelectedDeviceByPowerOn = Config.SetSelectedDeviceByPowerOn, - ShowAdvancedActions = Config.ShowAdvancedActions, - ShutdownDelay = Config.ShutdownDelay, - }; - } - - public bool UpdateConfig(SamsungServiceConfigDto config) - { - Config.Update(config); - - SetShortcuts(SHORTCUTID_SAMSUNGQA, Config.QuickAccessShortcut); - - SaveConfig(); - - return true; - } - - public bool UpdatePreset(SamsungPreset specPreset) - { - var currentPreset = _presets.FirstOrDefault(p => p.id == specPreset.id); - - if (currentPreset != null) - { - currentPreset.Update(specPreset); - - SavePresets(); - - return true; - } - - var newPreset = new SamsungPreset(specPreset); - newPreset.name = specPreset.name; - - _presets.Add(newPreset); - - SavePresets(); - - return true; - } - - public List> GetInvokableActions() - { - return SelectedDevice?.GetInvokableActions(Config.ShowAdvancedActions).Select(a => new InvokableActionDto - { - Advanced = a.Advanced, - Category = Utils.FirstCharUpperCase(a.Category ?? "Misc"), - CurrentValue = a.CurrentValue, - EnumType = a.EnumType, - MaxValue = a.MaxValue, - MinValue = a.MinValue, - Name = a.Name, - NumberOfValues = a.NumberOfValues, - Preset = a.Preset, - Title = a.Title ?? Utils.FirstCharUpperCase(a.Name), - ValueLabels = a.ValueLabels - }).ToList(); - } - - public async Task ExecuteInvokableAction(InvokableActionDto invokableAction) - { - return await SelectedDevice?.ExecuteInvokableAction(invokableAction, _appContext, Config); - } - - public bool SetSelectedDevice(SamsungDeviceDto deviceDto) - { - if (deviceDto == null) - { - return false; - } - - var device = Devices?.FirstOrDefault(d => string.IsNullOrEmpty(deviceDto.MacAddress) ? d.IpAddress == deviceDto.IpAddress : d.MacAddress == deviceDto.MacAddress); - if (device == null) - { - return false; - } - - SelectedDevice = device; - - return true; - } - } + class SamsungService : ServiceBase, ISamsungService + { + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + public override string ServiceName => "Samsung"; + + protected override string PresetsBaseFilename => "SamsungPresets.json"; + + public SamsungServiceConfig Config { get; private set; } + public SamsungDevice SelectedDevice { get; set; } + + private bool _allowPowerOn; + private ServiceManager _serviceManager; + private readonly PowerEventDispatcher _powerEventDispatcher; + private readonly DisplayChangeEventDispatcher _displayChangeEventDispatcher; + private readonly SessionSwitchDispatcher _sessionSwitchDispatcher; + private readonly RestartDetector _restartDetector; + private readonly ProcessEventDispatcher _processEventDispatcher; + private readonly WinApiAdminService _winApiAdminService; + private string _configFilename; + private bool _poweredOffByScreenSaver; + private int _poweredOffByScreenSaverProcessId; + private List _samsungApps = new List(); + private Shared.Common.GlobalContext _appContext; + + public List Devices { get; private set; } + + public static readonly int SHORTCUTID_SAMSUNGQA = -204; + + public SamsungService( + GlobalContext globalContext, + ServiceManager serviceManager, + PowerEventDispatcher powerEventDispatcher, + DisplayChangeEventDispatcher displayChangeEventDispatcher, + SessionSwitchDispatcher sessionSwitchDispatcher, + RestartDetector restartDetector, + ProcessEventDispatcher processEventDispatcher, + WinApiAdminService winApiAdminService) : base(globalContext) + { + _appContext = globalContext; + _allowPowerOn = _appContext.StartUpParams.RunningFromScheduledTask; + _serviceManager = serviceManager; + _powerEventDispatcher = powerEventDispatcher; + _displayChangeEventDispatcher = displayChangeEventDispatcher; + _sessionSwitchDispatcher = sessionSwitchDispatcher; + _restartDetector = restartDetector; + _processEventDispatcher = processEventDispatcher; + _winApiAdminService = winApiAdminService; + SamsungPreset.GetDeviceName += SamsungPreset_GetDeviceName; + + LoadConfig(); + LoadPresets(); + } + + private string SamsungPreset_GetDeviceName(string macAddress) + { + var device = Devices?.FirstOrDefault(d => !string.IsNullOrEmpty(d.MacAddress) && d.MacAddress.Equals(macAddress)); + if (device != null) + { + return device.Name; + } + + return "Unknown: device not found"; + } + + public static async Task ExecutePresetAsync(string presetName) + { + try + { + var samsungService = Program.ServiceProvider.GetRequiredService(); + + await samsungService.RefreshDevices(afterStartUp: true); + + var result = await samsungService.ApplyPreset(presetName); + + if (!result) + { + Console.WriteLine("Preset not found or error while executing."); + } + + return result; + } + catch (Exception ex) + { + Console.WriteLine("Error executing preset: " + ex.ToLogString()); + return false; + } + } + + public override void InstallEventHandlers() + { + base.InstallEventHandlers(); + + SetShortcuts(SHORTCUTID_SAMSUNGQA, Config.QuickAccessShortcut); + + _powerEventDispatcher.RegisterAsyncEventHandler(PowerEventDispatcher.Event_Startup, PowerStateChanged); + _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_MonitorOn, PowerModeChanged); + _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_MonitorOff, PowerModeChanged); + _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_Suspend, PowerModeChanged); + _powerEventDispatcher.RegisterAsyncEventHandler(PowerEventDispatcher.Event_Resume, PowerModeResume); + _powerEventDispatcher.RegisterEventHandler(PowerEventDispatcher.Event_Shutdown, PowerModeChanged); + _processEventDispatcher.RegisterAsyncEventHandler(ProcessEventDispatcher.Event_ProcessChanged, ProcessChanged); + _displayChangeEventDispatcher.RegisterAsyncEventHandler(DisplayChangeEventDispatcher.Event_DisplayChanged, DisplayChanged); + _sessionSwitchDispatcher.RegisterAsyncEventHandler(SessionSwitchDispatcher.Event_SessionSwitch, SessionSwitched); + } + + public override List GetInfo() + { + return [$"{_presets.Count} presets", $"{Devices?.Count ?? 0} devices"]; + } + + private async Task DisplayChanged(object sender, DisplayChangedEventArgs e, CancellationToken ct) + { + await ExecutePresetsForEvent(PresetTriggerType.DisplayChange); + } + + private async Task PowerStateChanged(object sender, PowerStateChangedEventArgs e, CancellationToken token) + { + await RefreshDevices(afterStartUp: true); + + if (_appContext.StartUpParams.RunningFromScheduledTask) + { + await ExecutePresetsForEvent(PresetTriggerType.Startup); + } + + if (_appContext.StartUpParams.ExecuteSamsungPreset) + { + await ExecutePresetAsync(_appContext.StartUpParams.SamsungPresetName); + } + } + + private async Task SessionSwitched(object sender, SessionSwitchEventArgs e, CancellationToken token) + { + if (e.Reason == SessionSwitchReason.SessionLock) + { + var devices = Devices.Where(d => d.Options.PowerOffAfterSessionLock); + PowerOffDevices(devices, PowerOnOffState.StandBy); + } + else if (e.Reason == SessionSwitchReason.SessionUnlock) + { + var devices = Devices.Where(d => d.Options.PowerOnAfterSessionUnlock).ToList(); + await PowerOnDevicesTask(devices); + } + } + + private void LoadConfig() + { + _configFilename = Path.Combine(_dataPath, "SamsungConfig.json"); + try + { + if (File.Exists(_configFilename)) + { + Config = JsonConvert.DeserializeObject(File.ReadAllText(_configFilename)); + } + } + catch (Exception ex) + { + Logger.Error($"LoadConfig: {ex.Message}"); + } + Config ??= new SamsungServiceConfig(); + } + + private async Task PowerModeResume(object sender, PowerStateChangedEventArgs e, CancellationToken _) + { + Logger.Debug($"PowerModeChanged: {e.State}"); + + await WakeAfterStartupOrResume(PowerOnOffState.Resume); + + await ExecutePresetsForEvent(PresetTriggerType.Resume); + } + + private void PowerModeChanged(object sender, PowerStateChangedEventArgs e) + { + Logger.Debug($"PowerModeChanged: {e.State}"); + + if (Devices?.Any() != true) + { + Logger.Debug("Devices have not been loaded, ignoring event"); + return; + } + + switch (e.State) + { + case PowerOnOffState.StandBy: + { + //_ = ExecutePresetsForEvent(PresetTriggerType.Standby).ConfigureAwait(true); + + var devices = Devices.Where(d => d.Options.PowerOffOnStandby); + PowerOffDevices(devices, PowerOnOffState.StandBy); + break; + } + case PowerOnOffState.ShutDown: + { + _ = ExecutePresetsForEvent(PresetTriggerType.Shutdown).ConfigureAwait(true); + + var devices = Devices.Where(d => d.Options.PowerOffOnShutdown); + + PowerOffDevices(devices); + break; + } + + case PowerOnOffState.MonitorOff: + { + var devices = Devices.Where(d => d.Options.PowerOffByWindows).ToList(); + PowerOffDevices(devices, PowerOnOffState.MonitorOff); + break; + } + case PowerOnOffState.MonitorOn: + { + var devices = Devices.Where(d => d.Options.PowerOnByWindows).ToList(); + PowerOnDevicesTask(devices); + break; + } + } + } + + private async Task ExecutePresetsForEvent(PresetTriggerType triggerType) + { + Logger.Debug($"Executing presets for event {triggerType}"); + + var presets = _presets.Where(p => p.Triggers.Any(t => t.Trigger == triggerType)).ToList(); + + if (!presets.Any()) + { + return; + } + + var applicableDevices = Devices.Where(d => d.Options.TriggersEnabled && + presets.Any(p => (string.IsNullOrEmpty(p.DeviceMacAddress) && d == SelectedDevice) || p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase))).ToList(); + + if (!applicableDevices.Any()) + { + return; + } + + await ExecuteEventPresets(_serviceManager, new[] { triggerType }).ConfigureAwait(true); + } + + public async Task RefreshDevices(bool connect = true, bool afterStartUp = false) + { + Devices = Config.Devices; + + if (!_globalContext.StartUpParams.NoDeviceRefresh) + { + var customIpAddresses = Devices.Where(d => d.IsCustom).Select(d => d.IpAddress); + + var pnpDevices = await Utils.GetPnpDevices(Config.DeviceSearchKey, Utils.DEV_CLASS_DISPLAY_TV_LCD); + + var autoDevices = pnpDevices.Where(p => !customIpAddresses.Contains(p.IpAddress)).Select(d => new SamsungDevice(d.Name, d.IpAddress, d.MacAddress, false)).ToList(); + var autoIpAddresses = pnpDevices.Select(d => d.IpAddress); + + Devices.RemoveAll(d => !d.IsCustom && !autoIpAddresses.Contains(d.IpAddress)); + + var newAutoDevices = autoDevices.Where(ad => ad.IpAddress != null && !Devices.Any(d => d.IpAddress != null && d.IpAddress.Equals(ad.IpAddress))); + Devices.AddRange(newAutoDevices); + } + + if (Devices.Any()) + { + var preferredDevice = Devices.FirstOrDefault(x => x.MacAddress != null && x.MacAddress.Equals(Config.PreferredMacAddress)) ?? Devices[0]; + + SelectedDevice = preferredDevice; + + SelectedDevice.Connected += SelectedDevice_Connected; + } + else + { + SelectedDevice = null; + } + + if (afterStartUp && SelectedDevice == null && Config.PowerOnAfterStartup && !string.IsNullOrEmpty(Config.PreferredMacAddress)) + { + Logger.Debug("No device has been found, trying to wake it first..."); + + var tempDevice = new SamsungDevice("Test", string.Empty, Config.PreferredMacAddress); + tempDevice.Wake(); + + await Task.Delay(4000); + return await RefreshDevices(); + } + + if (connect && SelectedDevice != null) + { + if (_allowPowerOn) + { + await WakeAfterStartupOrResume(); + } + else + { + var _ = SelectedDevice.ConnectAsync(); + } + } + + return true; + } + + private void SelectedDevice_Connected(object sender) + { + if (_samsungApps.Any()) + { + return; + } + + //await RefreshAppsAsync(); + } + + protected override List GetDefaultPresets() + { + return new List(); + } + + private void SaveConfig() + { + Config.PowerOnAfterStartup = Devices?.Any(d => d.Options.PowerOnAfterStartup) ?? false; + + Utils.WriteObject(_configFilename, Config); + } + + public void GlobalSave() + { + SavePresets(); + SaveConfig(); + } + + public SamsungDevice GetPresetDevice(SamsungPreset preset) + { + var device = string.IsNullOrEmpty(preset.DeviceMacAddress) + ? SelectedDevice + : Devices.FirstOrDefault(d => d.MacAddress?.Equals(preset.DeviceMacAddress, StringComparison.OrdinalIgnoreCase) ?? false); + return device; + } + + public override async Task ApplyPreset(SamsungPreset preset) + { + return await ApplyPreset(preset, _appContext); + } + + private async Task ApplyPreset(SamsungPreset preset, Shared.Common.GlobalContext appContext) + { + var result = true; + + var device = GetPresetDevice(preset); + + if (device == null) + { + return false; + } + + await device.ExecutePresetAsync(preset, appContext, Config); + + _lastAppliedPreset = preset; + + PresetApplied(); + + return result; + } + + internal void AddCustomDevice(SamsungDevice device) + { + Devices.Add(device); + } + + internal void RemoveCustomDevice(SamsungDevice device) + { + Devices.Remove(device); + + if (!string.IsNullOrEmpty(device.MacAddress)) + { + var presets = _presets.Where(p => !string.IsNullOrEmpty(p.DeviceMacAddress) && p.DeviceMacAddress.Equals(device.MacAddress, StringComparison.OrdinalIgnoreCase)).ToList(); + presets.ForEach(preset => + { + preset.DeviceMacAddress = null; + }); + } + + if (SelectedDevice == device) + { + SelectedDevice = Devices.Any() ? Devices.First() : null; + } + } + + private Task WakeAfterStartupOrResume(PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) + { + Devices.ForEach(d => d.ClearPowerOffTask()); + + var wakeDevices = Devices.Where(d => state == PowerOnOffState.StartUp && d.Options.PowerOnAfterStartup || + state == PowerOnOffState.Resume && d.Options.PowerOnAfterResume || + state == PowerOnOffState.ScreenSaver && d.Options.PowerOnAfterScreenSaver); + + return PowerOnDevicesTask(wakeDevices, state, checkUserSession); + } + + private void PowerOffDevices(IEnumerable devices, PowerOnOffState state = PowerOnOffState.ShutDown) + { + if (!devices.Any()) + { + return; + } + + if (NativeMethods.GetAsyncKeyState(NativeConstants.VK_CONTROL) < 0 || NativeMethods.GetAsyncKeyState(NativeConstants.VK_RCONTROL) < 0) + { + Logger.Debug("Not powering off because CONTROL-key is down"); + return; + } + + if (state == PowerOnOffState.ShutDown) + { + var sleep = Config.ShutdownDelay; + + Logger.Debug($"PowerOffDevices: Waiting for a maximum of {sleep} milliseconds..."); + + while (sleep > 0 && _restartDetector?.PowerOffDetected == false) + { + Thread.Sleep(100); + + if (_restartDetector != null && (_restartDetector.RestartDetected || _restartDetector.IsRebootInProgress())) + { + Logger.Debug("Not powering off because of a restart"); + return; + } + + sleep -= 100; + } + } + + Logger.Debug("Powering off tv(s)..."); + + var tasks = new List(); + foreach (var device in devices) + { + var task = device.PowerOffAsync(); + + tasks.Add(task); + } + + if (state == PowerOnOffState.StandBy) + { + var task = Task.Run(() => + { + var standByScript = Path.Combine(Program.DataDir, "StandByScript.bat"); + if (File.Exists(standByScript)) + { + _winApiAdminService.StartProcess(standByScript, hidden: true, wait: true, useShellExecute: false); + } + }); + + tasks.Add(task); + } + + // We can't use async here because we need to stay on the main thread... + var i = 0; + while (tasks.Any(t => t.Status < TaskStatus.WaitingForChildrenToComplete) && i < 200) + { + Thread.Sleep(10); + System.Windows.Forms.Application.DoEvents(); + i++; + } + + //var _ = Task.WhenAll(tasks.ToArray()); + + Logger.Debug("Done powering off tv(s)"); + } + + private Task PowerOnDevicesTask(IEnumerable devices, PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) + { + return Task.Run(async () => await PowerOnDevices(devices, state, checkUserSession)); + } + + private async Task PowerOnDevices(IEnumerable devices, PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) + { + if (!devices.Any()) + { + return; + } + + if (checkUserSession && !_sessionSwitchDispatcher.UserLocalSession) + { + Logger.Debug($"WakeAfterStartupOrResume: not waking because session info indicates no local session"); + return; + } + + var resumeScript = Path.Combine(Program.DataDir, "ResumeScript.bat"); + + var tasks = new List(); + + foreach (var device in devices) + { + if (!device.Options.PowerOnAfterManualPowerOff && device.PoweredOffBy == PowerOffSource.External) + { + Logger.Debug($"[{device.Name}]: device was manually powered off by user, not powering on"); + continue; + } + + if (state == PowerOnOffState.ScreenSaver) + { + tasks.Add(device.PerformActionAfterScreenSaver(Config.PowerOnRetries)); + continue; + } + + var task = device.WakeAndConnectWithRetries(Config.PowerOnRetries); + tasks.Add(task); + } + + await Task.WhenAll(tasks.ToArray()); + + if (File.Exists(resumeScript)) + { + _winApiAdminService.StartProcess(resumeScript, hidden: true); + } + } + + private async Task RefreshAppsAsync(bool force = false) + { + if (SelectedDevice == null) + { + Logger.Debug("Cannot refresh apps: no device has been selected"); + return; + } + + var apps = await SelectedDevice.GetAppsAsync(force); + + if (apps.Any()) + { + _samsungApps.Clear(); + _samsungApps.AddRange(apps); + } + } + + internal List GetApps() + { + return _samsungApps; + } + + private async Task ProcessChanged(object sender, ProcessChangedEventArgs args, CancellationToken token) + { + try + { + var wasConnected = Devices?.Any(d => d.IsConnected()); + + if (!wasConnected.HasValue) + { + return; + } + + var applicableDevices = Devices.Where(d => + d.Options.PowerOffOnScreenSaver + || d.Options.PowerOnAfterScreenSaver + || (d.Options.TriggersEnabled + // only these trigger types are ever triggered by a process changed event + && _presets.Any(p => p.Triggers.Any(t => t.Trigger == PresetTriggerType.ProcessSwitch || t.Trigger == PresetTriggerType.ScreensaverStart || t.Trigger == PresetTriggerType.ScreensaverStop) + && ((string.IsNullOrEmpty(p.DeviceMacAddress) && d == SelectedDevice) || p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase))))) + .ToList(); + + if (!applicableDevices.Any()) + { + return; + } + + if (_poweredOffByScreenSaver && !_sessionSwitchDispatcher.UserLocalSession) + { + return; + } + + var processes = args.RunningProcesses; + + if (_poweredOffByScreenSaver) + { + if (processes.Any(p => p.Id == _poweredOffByScreenSaverProcessId)) + { + return; + } + + var process = processes.FirstOrDefault(p => p.ProcessName.ToLowerInvariant().EndsWith(".scr")); + + if (process != null) + { + Logger.Debug($"Detected screen saver process switch from {_poweredOffByScreenSaverProcessId} to {process.Id}"); + + _poweredOffByScreenSaverProcessId = process.Id; + return; + } + + Logger.Debug("Screensaver stopped, waking"); + _poweredOffByScreenSaver = false; + _poweredOffByScreenSaverProcessId = 0; + + await WakeAfterStartupOrResume(PowerOnOffState.ScreenSaver); + + await ExecuteScreenSaverPresetsCustom(args, applicableDevices); + + return; + } + + var connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); + + if (!connectedDevices.Any()) + { + if (wasConnected == true) + { + Logger.Debug("Process monitor: TV(s) where connected, but not any longer"); + wasConnected = false; + + return; + } + else + { + var devices = applicableDevices.ToList(); + foreach (var device in devices) + { + //Logger.Debug($"Process monitor: test connection with {device.Name}..."); + + var test = await device.TestConnectionAsync(); + //Logger.Debug("Process monitor: test connection result: " + test); + } + + connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); + + if (!connectedDevices.Any()) + { + return; + } + } + } + + if (wasConnected == false) + { + Logger.Debug("Process monitor: TV(s) where not connected, but connection has now been established"); + wasConnected = true; + } + + if (connectedDevices.Any(d => d.Options.PowerOffOnScreenSaver || d.Options.PowerOnAfterScreenSaver)) + { + await HandleScreenSaverProcessAsync(processes, connectedDevices); + } + + await ExecuteProcessPresets(args, connectedDevices); + + await ExecuteScreenSaverPresetsCustom(args, connectedDevices); + } + catch (Exception ex) + { + Logger.Error("ProcessChanged: " + ex.ToLogString()); + } + } + + private async Task ExecuteScreenSaverPresetsCustom(ProcessChangedEventArgs context, IList connectedDevices) + { + var triggerDevices = connectedDevices.Where(d => d.Options.TriggersEnabled).ToList(); + + if (!triggerDevices.Any()) + { + return; + } + + await ExecuteScreenSaverPresets(_serviceManager, context); + } + + private async Task ExecuteProcessPresets(ProcessChangedEventArgs context, IList connectedDevices) + { + var triggerDevices = connectedDevices.Where(d => d.Options.TriggersEnabled).ToList(); + + if (context.IsScreenSaverActive) + { + return; + } + + var selectedDevice = SelectedDevice; + + var presets = _presets.Where(p => p.Triggers.Any(t => t.Trigger == PresetTriggerType.ProcessSwitch) && + ((string.IsNullOrEmpty(p.DeviceMacAddress) && selectedDevice?.Options.TriggersEnabled == true) + || Devices.Any(d => d.Options.TriggersEnabled && p.DeviceMacAddress.Equals(d.MacAddress, StringComparison.OrdinalIgnoreCase)))); + if (!presets.Any()) + { + return; + } + + var changedProcesses = new List(); + if (context.ForegroundProcess != null) + { + changedProcesses.Add(context.ForegroundProcess); + } + + if (context.ForegroundProcess != null && context.ForegroundProcessIsFullScreen) + { + presets = presets.Where(p => p.Triggers.Any(t => t.Conditions.HasFlag(PresetConditionType.FullScreen))); + } + else if (context.StoppedFullScreen) + { + presets = presets.Where(p => p.Triggers.Any(t => !t.Conditions.HasFlag(PresetConditionType.FullScreen))); + } + + var triggerContext = await CreateTriggerContext(_serviceManager, context); + + var toApplyPresets = presets.Where(p => p.Triggers.Any(t => t.TriggerActive(triggerContext))).ToList(); + + if (toApplyPresets.Any()) + { + var toApplyPreset = toApplyPresets.FirstOrDefault(p => toApplyPresets.Count == 1 || p.Triggers.Any(t => !t.IncludedProcesses.Contains("*"))) ?? toApplyPresets.First(); + + if (_lastTriggeredPreset != toApplyPreset) + { + Logger.Debug($"Applying triggered preset: {toApplyPreset.name} because trigger conditions are met"); + + await ApplyPreset(toApplyPreset); + + _lastTriggeredPreset = toApplyPreset; + } + } + } + + private async Task HandleScreenSaverProcessAsync(IList processes, List connectedDevices) + { + var process = processes.FirstOrDefault(p => p.ProcessName.ToLowerInvariant().EndsWith(".scr")); + + var powerOffDevices = connectedDevices.Where(d => d.Options.PowerOffOnScreenSaver).ToList(); + + if (process == null) + { + powerOffDevices.ForEach(d => d.ClearPowerOffTask()); + + return; + } + + var parent = process.Parent(processes); + + // Ignore manually started screen savers if no devices prefer this + if (parent?.ProcessName.Contains("winlogon") != true && !connectedDevices.Any(d => d.Options.HandleManualScreenSaver)) + { + return; + } + + Logger.Debug($"Screensaver started: {process.ProcessName}, parent: {parent.ProcessName}"); + try + { + foreach (var device in powerOffDevices) + { + Logger.Debug($"Screensaver check: test connection with {device.Name}..."); + + var test = await device.TestConnectionAsync(); + Logger.Debug("Screensaver check: test connection result: " + test); + if (!test) + { + continue; + } + + if (device.Options.ScreenSaverMinimalDuration > 0) + { + Logger.Debug($"Screensaver check: powering off tv {device.Name} in {device.Options.ScreenSaverMinimalDuration} seconds because of screensaver"); + + _poweredOffByScreenSaver = true; + + device.PowerOffIn(device.Options.ScreenSaverMinimalDuration); + + continue; + } + + Logger.Debug($"Screensaver check: powering off tv {device.Name} because of screensaver"); + try + { + _poweredOffByScreenSaver = await device.PerformActionOnScreenSaver().WaitAsync(TimeSpan.FromSeconds(5)); + } + catch (TimeoutException) + { + // Assume powering off was successful + _poweredOffByScreenSaver = true; + Logger.Error($"Screensaver check: timeout exception while powering off (probably connection closed)"); + } + catch (Exception pex) + { + // Assume powering off was successful + _poweredOffByScreenSaver = true; + Logger.Error($"Screensaver check: exception while powering off (probably connection closed): " + pex.Message); + } + Logger.Debug($"Screensaver check: powered off tv {device.Name}"); + } + + _poweredOffByScreenSaverProcessId = process.Id; + } + catch (Exception e) + { + Logger.Error($"Screensaver check: can't power off: " + e.ToLogString()); + } + } + + public async Task ExecuteEventPresets(PresetTriggerType triggerType) + { + await ExecuteEventPresets(_serviceManager, new[] { triggerType }); + } + + public List GetDevices() + { + return Devices?.Select(d => new SamsungDeviceDto + { + Name = d.Name, + MacAddress = d.MacAddress, + IpAddress = d.IpAddress, + IsCustom = d.IsCustom, + IsDummy = d.IsDummy, + Token = d.Token, + Options = d.Options, + IsConnected = d.IsConnected(), + IsSelected = d == SelectedDevice + }).ToList(); + } + + public async Task UpdateDevice(SamsungDeviceDto deviceSpec) + { + if (Devices == null) + { + return false; + } + + var device = Devices.FirstOrDefault(d => d.MacAddress == deviceSpec.MacAddress); + + if (device == null) + { + device = new SamsungDevice(deviceSpec); + + Devices.Add(device); + } + else + { + var reconnect = device.Update(deviceSpec); + + if (reconnect) + { + await device.ConnectAsync(true); + } + } + + SaveConfig(); + + return true; + } + + public async Task TestDevice(SamsungDeviceDto deviceSpec) + { + if (Devices == null) + { + return false; + } + + try + { + var device = new SamsungDevice(deviceSpec); + + return await device.TestConnectionAsync(); + } + catch + { + return false; + } + } + + public SamsungServiceConfigDto GetConfig() + { + return new SamsungServiceConfigDto + { + DefaultButtonDelay = Config.DefaultButtonDelay, + DeviceSearchKey = Config.DeviceSearchKey, + Devices = GetDevices(), + PowerOnAfterStartup = Config.PowerOnAfterStartup, + PowerOnRetries = Config.PowerOnRetries, + PreferredMacAddress = Config.PreferredMacAddress, + QuickAccessShortcut = Config.QuickAccessShortcut, + SetSelectedDeviceByPowerOn = Config.SetSelectedDeviceByPowerOn, + ShowAdvancedActions = Config.ShowAdvancedActions, + ShutdownDelay = Config.ShutdownDelay, + }; + } + + public bool UpdateConfig(SamsungServiceConfigDto config) + { + Config.Update(config); + + SetShortcuts(SHORTCUTID_SAMSUNGQA, Config.QuickAccessShortcut); + + SaveConfig(); + + return true; + } + + public bool UpdatePreset(SamsungPreset specPreset) + { + ValidatePreset(specPreset); + + var currentPreset = _presets.FirstOrDefault(p => p.id == specPreset.id); + + if (currentPreset != null) + { + currentPreset.Update(specPreset); + + SavePresets(currentPreset); + + return true; + } + + var newPreset = new SamsungPreset(specPreset); + newPreset.name = specPreset.name; + + _presets.Add(newPreset); + + SavePresets(newPreset); + + return true; + } + + public List> GetInvokableActions() + { + return SelectedDevice?.GetInvokableActions(Config.ShowAdvancedActions).Select(a => new InvokableActionDto + { + Advanced = a.Advanced, + Category = Utils.FirstCharUpperCase(a.Category ?? "Misc"), + CurrentValue = a.CurrentValue, + EnumType = a.EnumType, + MaxValue = a.MaxValue, + MinValue = a.MinValue, + Name = a.Name, + NumberOfValues = a.NumberOfValues, + Preset = a.Preset, + Title = a.Title ?? Utils.FirstCharUpperCase(a.Name), + ValueLabels = a.ValueLabels + }).ToList(); + } + + public async Task ExecuteInvokableAction(InvokableActionDto invokableAction) + { + return await SelectedDevice?.ExecuteInvokableAction(invokableAction, _appContext, Config); + } + + public bool SetSelectedDevice(SamsungDeviceDto deviceDto) + { + if (deviceDto == null) + { + return false; + } + + var device = Devices?.FirstOrDefault(d => string.IsNullOrEmpty(deviceDto.MacAddress) ? d.IpAddress == deviceDto.IpAddress : d.MacAddress == deviceDto.MacAddress); + if (device == null) + { + return false; + } + + SelectedDevice = device; + + return true; + } + } } \ No newline at end of file diff --git a/ColorControl/lgtv/LgTvApi.cs b/ColorControl/lgtv/LgTvApi.cs index 5475d97..aabefc6 100644 --- a/ColorControl/lgtv/LgTvApi.cs +++ b/ColorControl/lgtv/LgTvApi.cs @@ -8,479 +8,479 @@ using System.Threading.Tasks; namespace LgTv { - //https://mym.hackpad.com/ep/pad/static/rLlshKkzdNj - - public class LgTvApi : IDisposable - { - public bool ConnectionClosed { get => _connection?.ConnectionClosed ?? true; } - - private readonly LgTvApiCore _connection; - private LgWebOsMouseService _mouseConnection; - - private readonly ClientKeyStore _keyStore; - - private List _channelList; - private List _appList; - private string _ip; - private string _currentPairKey; - private string webSocketUri; - - protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - - public static async Task CreateLgTvApi(string ip, int retries = 1, bool useSecureWs = true) - { - var instance = new LgTvApi(ip, useSecureWs); - - return await instance.Connect(retries); - } - - private LgTvApi(string ip, bool useSecureWs) - { - webSocketUri = useSecureWs ? $"wss://{ip}:3001" : $"ws://{ip}:3000"; - _ip = ip; - _connection = new LgTvApiCore(); - _keyStore = new ClientKeyStore(ip); - } - - private async Task Connect(int retries) - { - Logger.Debug($"Trying to connect with websocket uri: {webSocketUri}"); - - while (retries > 0) - { - var connected = await Connect(); - if (connected) - { - await MakeHandShake(); - return this; - } - retries--; - await Task.Delay(500); - } - - Dispose(); - - return null; - } - - public string GetIpAddress() - { - return _ip; - } - - public async Task MakeHandShake() - { - var registerJson = Utils.GetResourceFile("LG_register.json"); - - _currentPairKey = _keyStore.GetClientKey(); - if (_currentPairKey != null && !_currentPairKey.All(k => k == '\0') && _keyStore.HasValidHandShake(registerJson)) - { - var key = registerJson.Replace("CLIENTKEYGOESHERE", _currentPairKey); - var conn = await _connection.SendCommandAsync(key); - _keyStore.SaveClientKey((string)conn.clientKey); - return; - } - - var registerJsonRemovedKey = registerJson.Replace("CLIENTKEYGOESHERE", string.Empty); - dynamic result = await _connection.SendCommandAsync(registerJsonRemovedKey); - _keyStore.SaveClientKey(result.clientKey); - _keyStore.SaveHandShake(registerJson); - } - - - public async Task Connect() - { - var ctx = _connection.Connect(new Uri(webSocketUri)); - return await ctx; - } - - public async Task GetMouse() - { - if (_mouseConnection != null) return _mouseConnection; - var requestMessage = new RequestMessage("ssap://com.webos.service.networkinput/getPointerInputSocket", new { }); - var s = await _connection.SendCommandAsync(requestMessage); - var socketPath = (string)s.socketPath; - - _mouseConnection = new LgWebOsMouseService(new LgTvApiCore()); - await _mouseConnection.Connect(socketPath); - return _mouseConnection; - } - - public async Task> ChannelList() - { - _channelList = new List(); - var requestMessage = new RequestMessage("channels", "ssap://tv/getChannelList"); - var command = _connection.SendCommandAsync(requestMessage); - var res = await command; - foreach (var c in res.channelList) - { - _channelList.Add(new Channel - { - Id = c.channelId, - Name = c.channelName, - Number = int.Parse((string)c.channelNumber) - }); - } - return _channelList.OrderBy(e => e.Number); - } - - public async Task GetChannel() - { - var requestMessage = new RequestMessage("channels", "ssap://tv/getCurrentChannel"); - var command = _connection.SendCommandAsync(requestMessage); - - var res = await command; - return new Channel() { Id = res.channelId, Name = res.channelName, Number = int.Parse(res.channelNumber) }; - } - public async Task ShowToast() - { - var requestMessage = new RequestMessage("ssap://system.notifications/createToast", new { message = "Co tam u Ciebie?" }); - await _connection.SendCommandAsync(requestMessage); - } - - //public async Task LaunchApp(string appId,Uri uri) - //{ - // var url = uri.ToString(); - // var requestMessage = new RequestMessage("ssap://system.launcher/launch", new { id = appId, @params={contentTarget } }); - // var response = await _connection.SendCommandAsync(requestMessage); - // return (string)response.sessionId; - //} - - public async Task VolumeDown() - { - var requestMessage = new RequestMessage("volumedown", "ssap://audio/volumeDown"); - await _connection.SendCommandAsync(requestMessage); - } - public async Task VolumeUp() - { - var requestMessage = new RequestMessage("volumeup", "ssap://audio/volumeUp"); - await _connection.SendCommandAsync(requestMessage); - } - public async Task ChannelInfo() - { - var requestMessage = new RequestMessage("programinfo", "ssap://tv/getChannelProgramInfo"); - await _connection.SendCommandAsync(requestMessage); - } - - public async Task SetChannel(string channelId) - { - var requestMessage = new RequestMessage("ssap://tv/openChannel", new { channelId }); - await _connection.SendCommandAsync(requestMessage); - } - public async Task SetMute(bool value) - { - var requestMessage = new RequestMessage("ssap://audio/setMute", new { mute = value }); - await _connection.SendCommandAsync(requestMessage); - } - public async Task ToogleMute() - { - await SetMute(!await IsMuted()); - } - - public async Task IsMuted() - { - var requestMessage = new RequestMessage("status", "ssap://audio/getStatus"); - var command = _connection.SendCommandAsync(requestMessage); - return (bool)(await command).mute; - } - - public async Task SetVolume(int value) - { - if (value >= 0 && value <= 100) - { - var requestMessage = new RequestMessage("ssap://audio/setVolume", new { volume = value }); - await _connection.SendCommandAsync(requestMessage); - } - } - public async Task TurnOff() - { - await _connection.SendCommandAsync(new RequestMessage("", "ssap://system/turnOff")); - } - - public async Task Reboot() - { - await ExecuteRequest("luna://com.webos.service.tv.power/reboot", new { reason = "" }); - } - - public async Task Play() - { - await _connection.SendCommandAsync(new RequestMessage("play", "ssap://media.controls/play")); - } - public async Task Pause() - { - await _connection.SendCommandAsync(new RequestMessage("pause", "ssap://media.controls/pause")); - } - - public async Task ChannelUp() - { - await _connection.SendCommandAsync(new RequestMessage("channelUp", "ssap://tv/channelUp")); - } - public async Task ChannelDown() - { - await _connection.SendCommandAsync(new RequestMessage("channelDown", "ssap://tv/channelDown")); - } - - public async Task Stop() - { - await _connection.SendCommandAsync(new RequestMessage("stop", "ssap://media.controls/stop")); - } - - public async Task TurnOn3D() - { - await _connection.SendCommandAsync(new RequestMessage("3d", "ssap://com.webos.service.tv.display/set3DOn")); - } - public async Task TurnOff3D() - { - await _connection.SendCommandAsync(new RequestMessage("3d", "ssap://com.webos.service.tv.display/set3DOff")); - } - - public async Task TurnScreenOff() - { - await Logger.SwallowAsync(() => _connection.SendCommandAsync(new RequestMessage("", "ssap://com.webos.service.tvpower/power/turnOffScreen"))); - } - - public async Task TurnScreenOn() - { - await Logger.SwallowAsync(() => _connection.SendCommandAsync(new RequestMessage("", "ssap://com.webos.service.tvpower/power/turnOnScreen"))); - } - - public async Task IsTurnedOn3D() - { - //Response: { returnValue: true, status3D: { status: true, pattern: ’2Dto3D’ } } - var requestMessage = new RequestMessage("status3D", "ssap://com.webos.service.tv.display/get3DStatus"); - var o = await _connection.SendCommandAsync(requestMessage); - return (bool)o.status3D.status; - } - - public async Task GetVolume() - { - // { - // "type": "response", - // "id": "status_1", - // "payload": { - // "muted": false, - // "scenario": "mastervolume_tv_speaker", - // "active": false, - // "action": "requested", - // "volume": 7, - // "returnValue": true, - // "subscribed": true - // } - // } - var requestMessage = new RequestMessage("status", "ssap://audio/getVolume"); - var o = await _connection.SendCommandAsync(requestMessage); - return ((bool)o.muted) ? -1 : (int)o.volume; - } - public async Task> GetInputList() - { - var requestMessage = new RequestMessage("input", "ssap://tv/getExternalInputList"); - var results = await _connection.SendCommandAsync(requestMessage); - var l = new List(); - foreach (var result in results) - { - l.Add(new ExternalInput(result.id, result.label) - { - Icon = result.icon - }); - } - return l; - } - - public async Task SetInput(string id) - { - var requestMessage = new RequestMessage("ssap://tv/switchInput", new { inputId = id }); - await _connection.SendCommandAsync(requestMessage); - } - - public async Task SubscribeVolume(Func callback, dynamic payload = null) - { - var requestMessage = new RequestMessage("ssap://audio/getVolume", null, "subscribe"); - - await _connection.SubscribeAsync(requestMessage, callback); - } - - public async Task SubscribePowerState(Func callback, dynamic payload = null) - { - var requestMessage = new RequestMessage("ssap://com.webos.service.tvpower/power/getPowerState", null, "subscribe"); - - await _connection.SubscribeAsync(requestMessage, callback); - } - - public async Task SubscribeForegroundApp(Func callback, dynamic payload = null) - { - var requestMessage = new RequestMessage("ssap://com.webos.applicationManager/getForegroundAppInfo", null, "subscribe"); - - await _connection.SubscribeAsync(requestMessage, callback); - } - - public async Task SubscribePictureSettings(Func callback, dynamic payload = null) - { - if (payload == null) - { - var keys = @"[ ""contrast"", ""backlight"", ""brightness"", ""color"" ]"; - - var json = @"{ ""category"": ""picture"", ""keys"": " + keys + @", ""subscribe"": true }"; - - payload = JObject.Parse(json); - } - - var requestMessage = new RequestMessage("ssap://settings/getSystemSettings", payload, "subscribe"); - - await _connection.SubscribeAsync(requestMessage, callback); - } - - public async Task> GetLaunchPoints() - { - _appList = new List(); - var requestMessage = new RequestMessage("launcher", "ssap://com.webos.applicationManager/listLaunchPoints"); - var command = _connection.SendCommandAsync(requestMessage); - var res = await command; - foreach (var c in res.launchpoints) - { - _appList.Add(new App - { - Id = c.id, - LaunchPointId = c.launchPointId, - Title = c.title, - Icon = c.icon - }); - } - return _appList.OrderBy(e => e.Title); - } - - public async Task> GetServiceList() - { - var list = new List(); - var requestMessage = new RequestMessage("channels", "ssap://api/getServiceList"); - var command = _connection.SendCommandAsync(requestMessage); - var res = await command; - return list; - } - - public async Task> GetApps(bool force = false) - { - var appList = new List(); - - dynamic res; - - var filename = Path.Combine(Utils.GetDataPath(), _ip + "_listApps.json"); - if (!force && File.Exists(filename)) - { - var json = File.ReadAllText(filename); - res = JToken.Parse(json); - } - else - { - var requestMessage = new RequestMessage("ssap://com.webos.applicationManager/listApps", new { }); - var command = _connection.SendCommandAsync(requestMessage); - res = await command; - - var json = Convert.ToString(res); - File.WriteAllText(filename, json); - } - - foreach (var c in res.apps) - { - appList.Add(new LgApp((string)c.id, (string)c.title)); - } - - return appList.OrderBy(e => e.title); - } - - public async Task LaunchApp(string appId, dynamic @params = null) - { - dynamic payload; - if (@params != null) - { - payload = new { id = appId, @params = @params }; - } - else - { - payload = new { id = appId }; - } - - var requestMessage = new RequestMessage("ssap://system.launcher/launch", payload); - var response = await _connection.SendCommandAsync(requestMessage); - return (string)response.sessionId; - } - public async Task CloseApp(string appId) - { - var requestMessage = new RequestMessage("ssap://system.launcher/close", new { id = appId }); - var response = await _connection.SendCommandAsync(requestMessage); - return (string)response.sessionId; - } - public async Task OpenWebBrowser(Uri uri) - { - var url = uri.ToString(); - var requestMessage = new RequestMessage("ssap://system.launcher/open", new { target = url }); - var response = await _connection.SendCommandAsync(requestMessage); - return (string)response.sessionId; - } - - public async Task LaunchYouTube(string videoId) - { - return await LaunchYouTube(new Uri($"http://www.youtube.com/tv?v={videoId}")); - } - public async Task LaunchYouTube(Uri uri) - { - var url = uri.ToString(); - var requestMessage = new RequestMessage("ssap://system.launcher/launch", new { id = "youtube.leanback.v4", @params = new { contentTarget = url } }); - var response = await _connection.SendCommandAsync(requestMessage); - return (string)response.sessionId; - } - - public async Task SetSystemSettings(string key, object value, string category = "picture") - { - var jsonValue = ParamToJson(value, ref key); - - var lunauri = "luna://com.webos.settingsservice/setSystemSettings"; - - var json = @"{ ""category"": """ + category + @""", ""settings"": { """ + key + @""": " + jsonValue + @" } }"; - - var @params = JObject.Parse(json); - - await ExecuteRequest(lunauri, @params); - } - - private static string ParamToJson(object value, ref string key, bool skipSeparator = false) - { - string jsonValue; - var valueType = value.GetType(); - var intType = typeof(int); - if (value.ToString().StartsWith("_")) - { - value = value.ToString().Substring(1); - } - if (value.ToString().StartsWith("bool_")) - { - value = bool.Parse(value.ToString().Substring(5)); - } - - if (valueType.IsArray && intType.IsAssignableFrom(valueType.GetElementType())) - { - var values = ((Array)value).Cast(); - jsonValue = "[" + string.Join(", ", values.Select(x => x.ToString()).ToArray()) + ']'; - } - else if (key?.Contains("_") == true && !skipSeparator) - { - var keys = key.Split('_'); - key = keys[0]; - var childKey = keys[1]; - - if (value is string strValue && strValue != "false" && strValue != "true" && Utils.ParseInt(strValue, int.MinValue) == int.MinValue) - { - value = $"\"{value}\""; - } - - jsonValue = @"{ """ + childKey + @""": " + value + " }"; - } - else if (value is bool) - { - jsonValue = value.ToString().ToLowerInvariant(); - } - else if (key.Equals("blackLevel", StringComparison.Ordinal)) - { - jsonValue = @"{ + //https://mym.hackpad.com/ep/pad/static/rLlshKkzdNj + + public class LgTvApi : IAsyncDisposable + { + public bool ConnectionClosed { get => _connection?.ConnectionClosed ?? true; } + + private readonly LgTvApiCoreCws _connection; + private LgWebOsMouseService _mouseConnection; + + private readonly ClientKeyStore _keyStore; + + private List _channelList; + private List _appList; + private string _ip; + private string _currentPairKey; + private string webSocketUri; + + protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + public static async Task CreateLgTvApi(string ip, int retries = 1, bool useSecureWs = true) + { + var instance = new LgTvApi(ip, useSecureWs); + + return await instance.Connect(retries); + } + + private LgTvApi(string ip, bool useSecureWs) + { + webSocketUri = useSecureWs ? $"wss://{ip}:3001" : $"ws://{ip}:3000"; + _ip = ip; + _connection = new LgTvApiCoreCws(); + _keyStore = new ClientKeyStore(ip); + } + + private async Task Connect(int retries) + { + Logger.Debug($"Trying to connect with websocket uri: {webSocketUri}"); + + while (retries > 0) + { + var connected = await Connect(); + if (connected) + { + await MakeHandShake(); + return this; + } + retries--; + await Task.Delay(500); + } + + await DisposeAsync(); + + return null; + } + + public string GetIpAddress() + { + return _ip; + } + + public async Task MakeHandShake() + { + var registerJson = Utils.GetResourceFile("LG_register.json"); + + _currentPairKey = _keyStore.GetClientKey(); + if (!string.IsNullOrWhiteSpace(_currentPairKey) && !_currentPairKey.All(k => k == '\0') && _keyStore.HasValidHandShake(registerJson)) + { + var key = registerJson.Replace("CLIENTKEYGOESHERE", _currentPairKey); + var conn = await _connection.SendCommandAsync(key); + _keyStore.SaveClientKey((string)conn.clientKey); + return; + } + + var registerJsonRemovedKey = registerJson.Replace("CLIENTKEYGOESHERE", string.Empty); + dynamic result = await _connection.SendCommandAsync(registerJsonRemovedKey); + _keyStore.SaveClientKey(result.clientKey); + _keyStore.SaveHandShake(registerJson); + } + + + public async Task Connect() + { + var ctx = _connection.Connect(new Uri(webSocketUri)); + return await ctx; + } + + public async Task GetMouse() + { + if (_mouseConnection != null) return _mouseConnection; + var requestMessage = new RequestMessage("ssap://com.webos.service.networkinput/getPointerInputSocket", new { }); + var s = await _connection.SendCommandAsync(requestMessage); + var socketPath = (string)s.socketPath; + + _mouseConnection = new LgWebOsMouseService(new LgTvApiCoreCws()); + await _mouseConnection.Connect(socketPath); + return _mouseConnection; + } + + public async Task> ChannelList() + { + _channelList = new List(); + var requestMessage = new RequestMessage("channels", "ssap://tv/getChannelList"); + var command = _connection.SendCommandAsync(requestMessage); + var res = await command; + foreach (var c in res.channelList) + { + _channelList.Add(new Channel + { + Id = c.channelId, + Name = c.channelName, + Number = int.Parse((string)c.channelNumber) + }); + } + return _channelList.OrderBy(e => e.Number); + } + + public async Task GetChannel() + { + var requestMessage = new RequestMessage("channels", "ssap://tv/getCurrentChannel"); + var command = _connection.SendCommandAsync(requestMessage); + + var res = await command; + return new Channel() { Id = res.channelId, Name = res.channelName, Number = int.Parse(res.channelNumber) }; + } + public async Task ShowToast() + { + var requestMessage = new RequestMessage("ssap://system.notifications/createToast", new { message = "Co tam u Ciebie?" }); + await _connection.SendCommandAsync(requestMessage); + } + + //public async Task LaunchApp(string appId,Uri uri) + //{ + // var url = uri.ToString(); + // var requestMessage = new RequestMessage("ssap://system.launcher/launch", new { id = appId, @params={contentTarget } }); + // var response = await _connection.SendCommandAsync(requestMessage); + // return (string)response.sessionId; + //} + + public async Task VolumeDown() + { + var requestMessage = new RequestMessage("volumedown", "ssap://audio/volumeDown"); + await _connection.SendCommandAsync(requestMessage); + } + public async Task VolumeUp() + { + var requestMessage = new RequestMessage("volumeup", "ssap://audio/volumeUp"); + await _connection.SendCommandAsync(requestMessage); + } + public async Task ChannelInfo() + { + var requestMessage = new RequestMessage("programinfo", "ssap://tv/getChannelProgramInfo"); + await _connection.SendCommandAsync(requestMessage); + } + + public async Task SetChannel(string channelId) + { + var requestMessage = new RequestMessage("ssap://tv/openChannel", new { channelId }); + await _connection.SendCommandAsync(requestMessage); + } + public async Task SetMute(bool value) + { + var requestMessage = new RequestMessage("ssap://audio/setMute", new { mute = value }); + await _connection.SendCommandAsync(requestMessage); + } + public async Task ToogleMute() + { + await SetMute(!await IsMuted()); + } + + public async Task IsMuted() + { + var requestMessage = new RequestMessage("status", "ssap://audio/getStatus"); + var command = _connection.SendCommandAsync(requestMessage); + return (bool)(await command).mute; + } + + public async Task SetVolume(int value) + { + if (value >= 0 && value <= 100) + { + var requestMessage = new RequestMessage("ssap://audio/setVolume", new { volume = value }); + await _connection.SendCommandAsync(requestMessage); + } + } + public async Task TurnOff() + { + await _connection.SendCommandAsync(new RequestMessage("", "ssap://system/turnOff")); + } + + public async Task Reboot() + { + await ExecuteRequest("luna://com.webos.service.tv.power/reboot", new { reason = "" }); + } + + public async Task Play() + { + await _connection.SendCommandAsync(new RequestMessage("play", "ssap://media.controls/play")); + } + public async Task Pause() + { + await _connection.SendCommandAsync(new RequestMessage("pause", "ssap://media.controls/pause")); + } + + public async Task ChannelUp() + { + await _connection.SendCommandAsync(new RequestMessage("channelUp", "ssap://tv/channelUp")); + } + public async Task ChannelDown() + { + await _connection.SendCommandAsync(new RequestMessage("channelDown", "ssap://tv/channelDown")); + } + + public async Task Stop() + { + await _connection.SendCommandAsync(new RequestMessage("stop", "ssap://media.controls/stop")); + } + + public async Task TurnOn3D() + { + await _connection.SendCommandAsync(new RequestMessage("3d", "ssap://com.webos.service.tv.display/set3DOn")); + } + public async Task TurnOff3D() + { + await _connection.SendCommandAsync(new RequestMessage("3d", "ssap://com.webos.service.tv.display/set3DOff")); + } + + public async Task TurnScreenOff() + { + await Logger.SwallowAsync(() => _connection.SendCommandAsync(new RequestMessage("", "ssap://com.webos.service.tvpower/power/turnOffScreen"))); + } + + public async Task TurnScreenOn() + { + await Logger.SwallowAsync(() => _connection.SendCommandAsync(new RequestMessage("", "ssap://com.webos.service.tvpower/power/turnOnScreen"))); + } + + public async Task IsTurnedOn3D() + { + //Response: { returnValue: true, status3D: { status: true, pattern: ’2Dto3D’ } } + var requestMessage = new RequestMessage("status3D", "ssap://com.webos.service.tv.display/get3DStatus"); + var o = await _connection.SendCommandAsync(requestMessage); + return (bool)o.status3D.status; + } + + public async Task GetVolume() + { + // { + // "type": "response", + // "id": "status_1", + // "payload": { + // "muted": false, + // "scenario": "mastervolume_tv_speaker", + // "active": false, + // "action": "requested", + // "volume": 7, + // "returnValue": true, + // "subscribed": true + // } + // } + var requestMessage = new RequestMessage("status", "ssap://audio/getVolume"); + var o = await _connection.SendCommandAsync(requestMessage); + return ((bool)o.muted) ? -1 : (int)o.volume; + } + public async Task> GetInputList() + { + var requestMessage = new RequestMessage("input", "ssap://tv/getExternalInputList"); + var results = await _connection.SendCommandAsync(requestMessage); + var l = new List(); + foreach (var result in results) + { + l.Add(new ExternalInput(result.id, result.label) + { + Icon = result.icon + }); + } + return l; + } + + public async Task SetInput(string id) + { + var requestMessage = new RequestMessage("ssap://tv/switchInput", new { inputId = id }); + await _connection.SendCommandAsync(requestMessage); + } + + public async Task SubscribeVolume(Func callback, dynamic payload = null) + { + var requestMessage = new RequestMessage("ssap://audio/getVolume", null, "subscribe"); + + await _connection.SubscribeAsync(requestMessage, callback); + } + + public async Task SubscribePowerState(Func callback, dynamic payload = null) + { + var requestMessage = new RequestMessage("ssap://com.webos.service.tvpower/power/getPowerState", null, "subscribe"); + + await _connection.SubscribeAsync(requestMessage, callback); + } + + public async Task SubscribeForegroundApp(Func callback, dynamic payload = null) + { + var requestMessage = new RequestMessage("ssap://com.webos.applicationManager/getForegroundAppInfo", null, "subscribe"); + + await _connection.SubscribeAsync(requestMessage, callback); + } + + public async Task SubscribePictureSettings(Func callback, dynamic payload = null) + { + if (payload == null) + { + var keys = @"[ ""contrast"", ""backlight"", ""brightness"", ""color"" ]"; + + var json = @"{ ""category"": ""picture"", ""keys"": " + keys + @", ""subscribe"": true }"; + + payload = JObject.Parse(json); + } + + var requestMessage = new RequestMessage("ssap://settings/getSystemSettings", payload, "subscribe"); + + await _connection.SubscribeAsync(requestMessage, callback); + } + + public async Task> GetLaunchPoints() + { + _appList = new List(); + var requestMessage = new RequestMessage("launcher", "ssap://com.webos.applicationManager/listLaunchPoints"); + var command = _connection.SendCommandAsync(requestMessage); + var res = await command; + foreach (var c in res.launchpoints) + { + _appList.Add(new App + { + Id = c.id, + LaunchPointId = c.launchPointId, + Title = c.title, + Icon = c.icon + }); + } + return _appList.OrderBy(e => e.Title); + } + + public async Task> GetServiceList() + { + var list = new List(); + var requestMessage = new RequestMessage("channels", "ssap://api/getServiceList"); + var command = _connection.SendCommandAsync(requestMessage); + var res = await command; + return list; + } + + public async Task> GetApps(bool force = false) + { + var appList = new List(); + + dynamic res; + + var filename = Path.Combine(Utils.GetDataPath(), _ip + "_listApps.json"); + if (!force && File.Exists(filename)) + { + var json = File.ReadAllText(filename); + res = JToken.Parse(json); + } + else + { + var requestMessage = new RequestMessage("ssap://com.webos.applicationManager/listApps", new { }); + var command = _connection.SendCommandAsync(requestMessage); + res = await command; + + var json = Convert.ToString(res); + File.WriteAllText(filename, json); + } + + foreach (var c in res.apps) + { + appList.Add(new LgApp((string)c.id, (string)c.title)); + } + + return appList.OrderBy(e => e.title); + } + + public async Task LaunchApp(string appId, dynamic @params = null) + { + dynamic payload; + if (@params != null) + { + payload = new { id = appId, @params = @params }; + } + else + { + payload = new { id = appId }; + } + + var requestMessage = new RequestMessage("ssap://system.launcher/launch", payload); + var response = await _connection.SendCommandAsync(requestMessage); + return (string)response.sessionId; + } + public async Task CloseApp(string appId) + { + var requestMessage = new RequestMessage("ssap://system.launcher/close", new { id = appId }); + var response = await _connection.SendCommandAsync(requestMessage); + return (string)response.sessionId; + } + public async Task OpenWebBrowser(Uri uri) + { + var url = uri.ToString(); + var requestMessage = new RequestMessage("ssap://system.launcher/open", new { target = url }); + var response = await _connection.SendCommandAsync(requestMessage); + return (string)response.sessionId; + } + + public async Task LaunchYouTube(string videoId) + { + return await LaunchYouTube(new Uri($"http://www.youtube.com/tv?v={videoId}")); + } + public async Task LaunchYouTube(Uri uri) + { + var url = uri.ToString(); + var requestMessage = new RequestMessage("ssap://system.launcher/launch", new { id = "youtube.leanback.v4", @params = new { contentTarget = url } }); + var response = await _connection.SendCommandAsync(requestMessage); + return (string)response.sessionId; + } + + public async Task SetSystemSettings(string key, object value, string category = "picture") + { + var jsonValue = ParamToJson(value, ref key); + + var lunauri = "luna://com.webos.settingsservice/setSystemSettings"; + + var json = @"{ ""category"": """ + category + @""", ""settings"": { """ + key + @""": " + jsonValue + @" } }"; + + var @params = JObject.Parse(json); + + await ExecuteRequest(lunauri, @params); + } + + private static string ParamToJson(object value, ref string key, bool skipSeparator = false) + { + string jsonValue; + var valueType = value.GetType(); + var intType = typeof(int); + if (value.ToString().StartsWith("_")) + { + value = value.ToString().Substring(1); + } + if (value.ToString().StartsWith("bool_")) + { + value = bool.Parse(value.ToString().Substring(5)); + } + + if (valueType.IsArray && intType.IsAssignableFrom(valueType.GetElementType())) + { + var values = ((Array)value).Cast(); + jsonValue = "[" + string.Join(", ", values.Select(x => x.ToString()).ToArray()) + ']'; + } + else if (key?.Contains("_") == true && !skipSeparator) + { + var keys = key.Split('_'); + key = keys[0]; + var childKey = keys[1]; + + if (value is string strValue && strValue != "false" && strValue != "true" && Utils.ParseInt(strValue, int.MinValue) == int.MinValue) + { + value = $"\"{value}\""; + } + + jsonValue = @"{ """ + childKey + @""": " + value + " }"; + } + else if (value is bool) + { + jsonValue = value.ToString().ToLowerInvariant(); + } + else if (key.Equals("blackLevel", StringComparison.Ordinal)) + { + jsonValue = @"{ ""ntsc"": ""auto"", ""ntsc443"": ""auto"", ""pal"": ""auto"", @@ -491,157 +491,158 @@ namespace LgTv ""unknown"": ""auto"" }"; - jsonValue = jsonValue.Replace("auto", value.ToString()); - } - else - { - if (key.Equals("arcPerApp") && value.ToString().StartsWith("_")) - { - value = value.ToString().Substring(1); - } + jsonValue = jsonValue.Replace("auto", value.ToString()); + } + else + { + if (key.Equals("arcPerApp") && value.ToString().StartsWith("_")) + { + value = value.ToString().Substring(1); + } - jsonValue = $"\"{value}\""; - } + jsonValue = $"\"{value}\""; + } - return jsonValue; - } + return jsonValue; + } - public async Task SetConfig(string key, object value) - { - var lunauri = "luna://com.webos.service.config/setConfigs"; + public async Task SetConfig(string key, object value) + { + var lunauri = "luna://com.webos.service.config/setConfigs"; - var jsonValue = ParamToJson(value, ref key); + var jsonValue = ParamToJson(value, ref key); - var @params = JObject.Parse(@"{ ""configs"": { """ + key + @""": " + jsonValue + @" } }"); + var @params = JObject.Parse(@"{ ""configs"": { """ + key + @""": " + jsonValue + @" } }"); - await ExecuteRequest(lunauri, @params); - } + await ExecuteRequest(lunauri, @params); + } - public async Task SetSystemProperty(string key, object value) - { - var lunauri = "luna://com.webos.service.tv.systemproperty/setProperties"; + public async Task SetSystemProperty(string key, object value) + { + var lunauri = "luna://com.webos.service.tv.systemproperty/setProperties"; - var jsonValue = ParamToJson(value, ref key, true); + var jsonValue = ParamToJson(value, ref key, true); - //var @params = JObject.Parse(@"{ ""keys"": { """ + key + @""": " + jsonValue + @" } }"); - var @params = JObject.Parse(@"{ """ + key + @""": " + jsonValue + @" }"); + //var @params = JObject.Parse(@"{ ""keys"": { """ + key + @""": " + jsonValue + @" } }"); + var @params = JObject.Parse(@"{ """ + key + @""": " + jsonValue + @" }"); - await ExecuteRequest(lunauri, @params); - } + await ExecuteRequest(lunauri, @params); + } - public async Task SetDeviceConfig(string id, string icon, string label) - { - var lunauri = "luna://com.webos.service.eim/setDeviceInfo"; + public async Task SetDeviceConfig(string id, string icon, string label) + { + var lunauri = "luna://com.webos.service.eim/setDeviceInfo"; - var iconPng = icon + ".png"; + var iconPng = icon + ".png"; - var @params = new - { - id = id, - label = label, - icon = iconPng - }; + var @params = new + { + id = id, + label = label, + icon = iconPng + }; - await ExecuteRequest(lunauri, @params); - } + await ExecuteRequest(lunauri, @params); + } - public async Task SetTpcOrGsr(string name, bool enable) - { - var lunauri = $"luna://com.webos.service.oledepl/{(name == "TPC" ? "setTemporalPeakControl" : "setGlobalStressReduction")}"; + public async Task SetTpcOrGsr(string name, bool enable) + { + var lunauri = $"luna://com.webos.service.oledepl/{(name == "TPC" ? "setTemporalPeakControl" : "setGlobalStressReduction")}"; - var @params = new - { - enable - }; + var @params = new + { + enable + }; - await ExecuteRequest(lunauri, @params); - } + await ExecuteRequest(lunauri, @params); + } - private async Task ExecuteRequest(string lunauri, object @params) - { - var buttons = new[] - { - new { - label = "", - onClick = lunauri, - @params = @params - } - }; + private async Task ExecuteRequest(string lunauri, object @params) + { + var buttons = new[] + { + new { + label = "", + onClick = lunauri, + @params = @params + } + }; - var payload = new - { - message = "Applying...", - buttons = buttons, - onclose = new { uri = lunauri, @params = @params }, - onfail = new { uri = lunauri, @params = @params } - }; + var payload = new + { + message = "Applying...", + buttons = buttons, + onclose = new { uri = lunauri, @params = @params }, + onfail = new { uri = lunauri, @params = @params } + }; - var requestMessage = new RequestMessage("ssap://system.notifications/createAlert", payload); - var response = await _connection.SendCommandAsync(requestMessage); + var requestMessage = new RequestMessage("ssap://system.notifications/createAlert", payload); + var response = await _connection.SendCommandAsync(requestMessage); - var alertId = (string)response.alertId; - if (alertId != null) - { - var closeAlert = new - { - alertId = alertId - }; - requestMessage = new RequestMessage("ssap://system.notifications/closeAlert", closeAlert); - await _connection.SendCommandAsync(requestMessage); - } - } + var alertId = (string)response.alertId; + if (alertId != null) + { + var closeAlert = new + { + alertId = alertId + }; + requestMessage = new RequestMessage("ssap://system.notifications/closeAlert", closeAlert); + await _connection.SendCommandAsync(requestMessage); + } + } - public async Task GetSystemInfo(params string[] keys) - { - var payload = new - { - keys = keys - }; + public async Task GetSystemInfo(params string[] keys) + { + var payload = new + { + keys = keys + }; - var requestMessage = new RequestMessage("ssap://system/getSystemInfo", payload); - var command = _connection.SendCommandAsync(requestMessage); - var res = await command; - return res; - } + var requestMessage = new RequestMessage("ssap://system/getSystemInfo", payload); + var command = _connection.SendCommandAsync(requestMessage); + var res = await command; + return res; + } - public async Task GetSystemSettings(string category, params string[] keys) - { - var payload = new - { - category = category, - keys = keys - }; + public async Task GetSystemSettings(string category, params string[] keys) + { + var payload = new + { + category = category, + keys = keys + }; - var requestMessage = new RequestMessage("ssap://settings/getSystemSettings", payload); - var command = _connection.SendCommandAsync(requestMessage); - var res = await command; - return res; - } + var requestMessage = new RequestMessage("ssap://settings/getSystemSettings", payload); + var command = _connection.SendCommandAsync(requestMessage); + var res = await command; + return res; + } - public async Task GetSystemSettings2(string category) - { - var payload = new - { - category = "dimensionInfo", - //dimension = new { input = "default", _3dStatus = "2d", dynamicRange = "sdr" }, - keys = new[] { "input", "_3dStatus", "colorSystem", "dynamicRange", "gameInput" } - }; - //await ExecuteRequest("luna://com.webos.settingsservice/getSystemSettings", payload); - //return null; + public async Task GetSystemSettings2(string category) + { + var payload = new + { + category = "dimensionInfo", + //dimension = new { input = "default", _3dStatus = "2d", dynamicRange = "sdr" }, + keys = new[] { "input", "_3dStatus", "colorSystem", "dynamicRange", "gameInput" } + }; + //await ExecuteRequest("luna://com.webos.settingsservice/getSystemSettings", payload); + //return null; - var requestMessage = new RequestMessage("ssap://settings/getSystemSettings", payload); - var command = _connection.SendCommandAsync(requestMessage); - var res = await command; - return res; - } + var requestMessage = new RequestMessage("ssap://settings/getSystemSettings", payload); + var command = _connection.SendCommandAsync(requestMessage); + var res = await command; + return res; + } - public void Close() - { - _connection.Close(); - } - public void Dispose() - { - _connection?.Dispose(); - } - } + public async ValueTask DisposeAsync() + { + if (_connection != null) + { + await _connection.DisposeAsync(); + } + + GC.SuppressFinalize(this); + } + } } \ No newline at end of file diff --git a/ColorControl/lgtv/LgTvConnection.cs b/ColorControl/lgtv/LgTvConnection.cs deleted file mode 100644 index 4a5d63b..0000000 --- a/ColorControl/lgtv/LgTvConnection.cs +++ /dev/null @@ -1,281 +0,0 @@ -using ColorControl.Shared.Common; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using Windows.Networking.Sockets; -using Windows.Security.Cryptography.Certificates; -using Windows.Storage.Streams; - -namespace LgTv -{ - public class SendMessageException : Exception - { - public SendMessageException(string message, Exception e) : base(message, e) - { - - } - } - - public delegate void IsConnectedDelegate(bool status); - - public class LgTvApiCore : IDisposable - { - public static SynchronizationContext SyncContext { get; set; } - - public bool ConnectionClosed { get; private set; } - - private MessageWebSocket _connection; - private DataWriter _messageWriter; - private int _commandCount; - - public event IsConnectedDelegate IsConnected; - private readonly ConcurrentDictionary> _tokens = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary> _callbacks = new ConcurrentDictionary>(); - - private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - - public async Task Connect(Uri uri, bool ignoreReceiver = false) - { - try - { - //Logger.Debug($"Connecting from thread: {Environment.CurrentManagedThreadId}"); - - _connection?.Dispose(); - - ConnectionClosed = false; - _commandCount = 0; - - // Not creating the web socket from main thread can cause a stackoverflow in combase32.dll - if (Environment.CurrentManagedThreadId != 1) - { - SyncContext.Send((_) => CreateWebSocket(), null); - } - else - { - CreateWebSocket(); - } - - try - { - await _connection.ConnectAsync(uri).AsTask().WaitAsync(TimeSpan.FromSeconds(5)); - - _messageWriter = new DataWriter(_connection.OutputStream); - - IsConnected?.Invoke(true); - return true; - } - catch (TimeoutException te) - { - Logger.Error($"Connect to {uri}: {te.Message}"); - return false; - } - } - catch (Exception e) - { - switch (SocketError.GetStatus(e.HResult)) - { - case SocketErrorStatus.HostNotFound: - // Handle HostNotFound Error - break; - default: - // Handle Unknown Error - break; - } - - Logger.Error($"Connect to {uri}: {e.Message}"); - return false; - } - } - - private void CreateWebSocket() - { - _connection = new MessageWebSocket(); - _connection.Control.IgnorableServerCertificateErrors.Add(ChainValidationResult.Untrusted); - _connection.Control.IgnorableServerCertificateErrors.Add(ChainValidationResult.InvalidName); - _connection.Control.MessageType = SocketMessageType.Utf8; - _connection.MessageReceived += Connection_MessageReceived; - _connection.Closed += Connection_Closed; - } - - public async Task SendMessageAsync(string message) - { - try - { - if (_messageWriter == null) - { - throw new Exception("SendMessageAsync: no writer"); - } - - _messageWriter.WriteString(message); - - await _messageWriter.StoreAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5)); - } - catch (Exception e) - { - Logger.Error("SendMessageAsync: " + e.Message); - ConnectionClosed = true; - _messageWriter?.Dispose(); - } - } - - public async Task SendCommandAsync(string message) - { - var obj = JsonConvert.DeserializeObject(message); - return await SendCommandAsync((string)obj.id, message); - } - - private async Task SendCommandAsync(string id, string message) - { - try - { - var taskSource = new TaskCompletionSource(); - _tokens.TryAdd(id, taskSource); - await SendMessageAsync(message); - if (ConnectionClosed) - { - throw new Exception("Connection closed"); - } - - return await taskSource.Task; - } - catch (Exception e) - { - throw new SendMessageException("Can't send message", e); - } - } - - public async Task SendCommandAsync(RequestMessage message) - { - var rawMessage = new RawRequestMessage(message, ++_commandCount); - var serialized = JsonConvert.SerializeObject(rawMessage, new JsonSerializerSettings() - { - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); - return await SendCommandAsync(rawMessage.Id, serialized); - } - - public async Task SubscribeAsync(RequestMessage message, Func callback) - { - var rawMessage = new RawRequestMessage(message, ++_commandCount); - var serialized = JsonConvert.SerializeObject(rawMessage, new JsonSerializerSettings() - { - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); - - _callbacks.TryAdd(rawMessage.Id, callback); - - return await SendCommandAsync(rawMessage.Id, serialized); - } - - private void Connection_Closed(IWebSocket sender, WebSocketClosedEventArgs args) - { - MessageWebSocket webSocket = Interlocked.Exchange(ref _connection, null); - webSocket?.Dispose(); - ConnectionClosed = true; - } - - private async void Connection_MessageReceived(MessageWebSocket sender, MessageWebSocketMessageReceivedEventArgs args) - { - try - { - var task = new Task(args.GetDataReader); - task.Start(); - var dataReader = await task.WaitAsync(TimeSpan.FromSeconds(5)); - - using (dataReader) - { - dataReader.UnicodeEncoding = UnicodeEncoding.Utf8; - var message = dataReader.ReadString(dataReader.UnconsumedBufferLength); - var obj = JsonConvert.DeserializeObject(message); - var id = (string)obj.id; - var type = (string)obj.type; - - TaskCompletionSource taskCompletion; - if (type == "registered") - { - if (_tokens.TryRemove(id, out taskCompletion)) - { - var key = (string)JObject.Parse(message)["payload"]["client-key"]; - taskCompletion.TrySetResult(new { clientKey = key }); - } - - } - else if (_tokens.TryGetValue(id, out taskCompletion)) - { - if (id == "register_0") return; - if (obj.type == "error") - { - taskCompletion.SetException(new Exception(obj.error?.ToString())); - } - //else if (args.Cancelled) - //{ - // taskSource.SetCanceled(); - //} - taskCompletion.TrySetResult(obj.payload); - - if (_callbacks.TryGetValue(id, out Func callback)) - { - try - { - callback(obj.payload); - } - catch (Exception callbackException) - { - Logger.Error($"Connection_MessageReceived: the callback threw an exception: {callbackException.ToLogString()}"); - } - } - } - } - } - catch (Exception ex) - { - var status = WebSocketError.GetStatus(ex.GetBaseException().HResult); - Logger.Error($"Connection_MessageReceived: status: {status}, exception: {ex.ToLogString()}"); - - SetExceptionOnAllTokens(ex); - - ConnectionClosed = true; - _messageWriter?.Dispose(); - } - } - - private void SetExceptionOnAllTokens(Exception ex) - { - //Logger.Debug($"Tokens: {_tokens.Count}"); - - foreach (var taskCompletion in _tokens.Values) - { - try - { - if (taskCompletion.Task.Status != TaskStatus.RanToCompletion) - { - Logger.Debug("taskCompletion.Task.Status: " + taskCompletion.Task.Status); - taskCompletion.SetException(new Exception(ex.Message)); - } - } - catch (Exception ex2) - { - Logger.Error("SetExceptionOnAllTokens: " + ex2.Message); - } - } - - _tokens.Clear(); - } - - public void Close() - { - _connection?.Close(1, ""); - } - - public void Dispose() - { - _connection?.Dispose(); - } - } -} diff --git a/ColorControl/lgtv/LgWebOsMouseService.cs b/ColorControl/lgtv/LgWebOsMouseService.cs index 0cf39a3..1e94bf9 100644 --- a/ColorControl/lgtv/LgWebOsMouseService.cs +++ b/ColorControl/lgtv/LgWebOsMouseService.cs @@ -4,49 +4,54 @@ using System.Threading.Tasks; namespace LgTv { - public class LgWebOsMouseService : IDisposable - { - private readonly LgTvApiCore _connection; + public class LgWebOsMouseService : IAsyncDisposable + { + private readonly LgTvApiCoreCws _connection; - public LgWebOsMouseService(LgTvApiCore connection) - { - _connection = connection; - } + public LgWebOsMouseService(LgTvApiCoreCws connection) + { + _connection = connection; + } - public async Task Connect(string uri) - { - var ctx = _connection.Connect(new Uri(uri), ignoreReceiver: true); - return (await ctx); - } + public async Task Connect(string uri) + { + var ctx = _connection.Connect(new Uri(uri), ignoreReceiver: true); + return (await ctx); + } - public async Task SendButton(ButtonType bt) - { - var text = bt.ToString(); - if (text[0] == '_') - { - text = text.Substring(1); - } - await _connection.SendMessageAsync($"type:button\nname:{text}\n\n"); - } + public async Task SendButton(ButtonType bt) + { + var text = bt.ToString(); + if (text[0] == '_') + { + text = text.Substring(1); + } + await _connection.SendMessageAsync($"type:button\nname:{text}\n\n"); + } - public void Move(double dx, double dy, bool drag = false) - { - _connection.SendMessageAsync($"type:move\ndx:{dx}\ndy:{dy}\ndown:{(drag ? 1 : 0)}\n\n").ConfigureAwait(false); - } + public void Move(double dx, double dy, bool drag = false) + { + _connection.SendMessageAsync($"type:move\ndx:{dx}\ndy:{dy}\ndown:{(drag ? 1 : 0)}\n\n").ConfigureAwait(false); + } - public void Scroll(double dx, double dy) - { - _connection.SendMessageAsync($"type:scroll\ndx:{dx}\ndy:{dy}\n\n").ConfigureAwait(false); - } + public void Scroll(double dx, double dy) + { + _connection.SendMessageAsync($"type:scroll\ndx:{dx}\ndy:{dy}\n\n").ConfigureAwait(false); + } - public void Click() - { - _connection.SendMessageAsync("type:click\n\n").ConfigureAwait(false); - } + public void Click() + { + _connection.SendMessageAsync("type:click\n\n").ConfigureAwait(false); + } - public void Dispose() - { - _connection?.Dispose(); - } - } + public async ValueTask DisposeAsync() + { + if (_connection != null) + { + await _connection.DisposeAsync(); + } + + GC.SuppressFinalize(this); + } + } } diff --git a/LittleCms/LittleCms.csproj b/LittleCms/LittleCms.csproj index 7ee1a89..99238f9 100644 --- a/LittleCms/LittleCms.csproj +++ b/LittleCms/LittleCms.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0-windows enable True diff --git a/MHC2Gen/IccContext.cs b/MHC2Gen/IccContext.cs index 5fcf9b5..0d0bfb9 100644 --- a/MHC2Gen/IccContext.cs +++ b/MHC2Gen/IccContext.cs @@ -9,1208 +9,1228 @@ using System.Text.Json; namespace MHC2Gen { - class MHC2Tag - { - public const TagSignature Signature = (TagSignature)0x4D484332; + class MHC2Tag + { + public const TagSignature Signature = (TagSignature)0x4D484332; - public double MinCLL { get; set; } - public double MaxCLL { get; set; } - public double[,]? Matrix3x4 { get; set; } - public double[,]? RegammaLUT { get; set; } + public double MinCLL { get; set; } + public double MaxCLL { get; set; } + public double[,]? Matrix3x4 { get; set; } + public double[,]? RegammaLUT { get; set; } - public MHC2Tag() { } + public MHC2Tag() { } - public MHC2Tag(ReadOnlySpan bytes) - { - LoadFromBytes(bytes); - } + public MHC2Tag(ReadOnlySpan bytes) + { + LoadFromBytes(bytes); + } - private void LoadFromBytes(ReadOnlySpan bytes) - { - var ms0 = new MemoryStream(bytes.ToArray()); - var reader = new BinaryReader(ms0); + private void LoadFromBytes(ReadOnlySpan bytes) + { + var ms0 = new MemoryStream(bytes.ToArray()); + var reader = new BinaryReader(ms0); - // Signature - reader.ReadBytes(4); - // 0 - reader.ReadBytes(4); + // Signature + reader.ReadBytes(4); + // 0 + reader.ReadBytes(4); - // Lut Size - var lutSize = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); + // Lut Size + var lutSize = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); - MinCLL = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4))); - MaxCLL = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4))); + MinCLL = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4))); + MaxCLL = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4))); - // Matrix offset - var maxtrixOffset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); + // Matrix offset + var maxtrixOffset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); - // Lut0 offset - var lut0Offset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); + // Lut0 offset + var lut0Offset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); - // Lut1 offset - var lut1Offset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); + // Lut1 offset + var lut1Offset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); - // Lut2 offset - var lut2Offset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); + // Lut2 offset + var lut2Offset = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); - Matrix3x4 = new double[3, 4]; + Matrix3x4 = new double[3, 4]; - for (var x = 0; x < 3; x++) - { - for (var y = 0; y < 4; y++) - { - Matrix3x4[x, y] = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4))); - } - } - - RegammaLUT = new double[3, lutSize]; - - for (var c = 0; c < 3; c++) - { - var discard = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(8)); - - for (var x = 0; x < lutSize; x++) - { - RegammaLUT[c, x] = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4))); - } - } - } - - public void ApplySdrAcm(double whiteLuminance = 120.0, double blackLuminance = 0.0, double gamma = 2.2, double boostPercentage = 0, double shadowDetailBoost = 0) - { - var lutSize = 1024; - var amplifier = boostPercentage == 0 ? 1 : 1 + boostPercentage / 100; - - RegammaLUT = new double[3, lutSize]; - - for (var i = 0; i < lutSize; i++) - { - var (_, value) = CmsFunctions.SrgbAcm(i, whiteLuminance, blackLuminance, gamma, lutSize - 1); - - // Average between sSRGB and Piecewise gamma - if (shadowDetailBoost > 0) - { - var piecewiseValue = (double)i / 1023; - - var correctedBoost = (shadowDetailBoost * (1023 - i)) / 1023; - - value = ((value * (100 - correctedBoost)) + (piecewiseValue * correctedBoost)) / 100; - } - - for (var c = 0; c < 3; c++) - { - //var tempPercentage = boostPercentage * ((double)i / lutSize); - - //var amplifier = tempPercentage == 0 ? 1 : tempPercentage > 0 ? 1 + tempPercentage / 100 : 1 - tempPercentage / 100; - - RegammaLUT[c, i] = value * amplifier; - } - } - } - - public void ApplyPiecewise(double boostPercentage = 0) - { - var lutSize = 1024; - var amplifier = boostPercentage == 0 ? 1 : 1 + boostPercentage / 100; - - RegammaLUT = new double[3, lutSize]; - - for (var i = 0; i < lutSize; i++) - { - var value = (double)i / 1023; - - for (var c = 0; c < 3; c++) - { - RegammaLUT[c, i] = value * amplifier; - } - } - } - - public void ApplyGamma() - { - var lutSize = 1024; - - RegammaLUT = new double[3, lutSize]; - - for (var i = 0; i < lutSize; i++) - { - var value = CmsFunctions.RgbToLinear((double)i / (lutSize - 1), 1.0); - - for (var c = 0; c < 3; c++) - { - RegammaLUT[c, i] = value; - } - } - } - - private static int EncodeS15F16(double value) - { - var x = (int)Math.Round(value * 65536); - return x; - } - - private static double DecodeS15F16(int value) - { - return (double)value / 65536; - } - - public byte[] ToBytes() - { - if (RegammaLUT!.GetLength(0) != 3) throw new ArrayTypeMismatchException(); - var lut_size = RegammaLUT.GetLength(1); - if (lut_size <= 1 || lut_size > 4096) throw new IndexOutOfRangeException(); - if (Matrix3x4!.Length != 12 || Matrix3x4.GetLength(0) != 3) throw new ArrayTypeMismatchException(); - var ms0 = new MemoryStream(); - var writer = new BinaryWriter(ms0); - // type - writer.Write(BinaryPrimitives.ReverseEndianness((int)Signature)); - writer.Write(BinaryPrimitives.ReverseEndianness(0)); - - writer.Write(BinaryPrimitives.ReverseEndianness(lut_size)); - writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(MinCLL))); - writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(MaxCLL))); - // matrix offset - writer.Write(BinaryPrimitives.ReverseEndianness(36)); - // lut0 offset - writer.Write(BinaryPrimitives.ReverseEndianness(84)); - // lut1 offset - var lut1_offset = 84 + 8 + lut_size * 4; - writer.Write(BinaryPrimitives.ReverseEndianness(lut1_offset)); - // lut2 offset - var lut2_offset = lut1_offset + 8 + lut_size * 4; - writer.Write(BinaryPrimitives.ReverseEndianness(lut2_offset)); - - foreach (var e in Matrix3x4) - { - writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(e))); - } - - for (int ch = 0; ch < 3; ch++) - { - writer.Write(new ReadOnlySpan(new byte[] { (byte)'s', (byte)'f', (byte)'3', (byte)'2', 0, 0, 0, 0 })); - for (int i = 0; i < lut_size; i++) - { - writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(RegammaLUT[ch, i]))); - } - } - writer.Flush(); - return ms0.ToArray(); - } - - internal static MHC2Tag? LoadFromProfile(IccProfile profile) - { - var bytes = profile.RawBytes; - - if (bytes == null) - { - return null; - } - - // 0x4D484332 - var index = -1; - var first = true; - for (var i = 0; i < bytes.Length; i += 4) - { - var chunk = bytes.AsSpan(new Range(i, i + 4)); - - if (chunk[0] == 77 && chunk[1] == 72 && chunk[2] == 67 && chunk[3] == 50) - { - index = i; - - if (first) - { - index = -1; - first = false; - continue; - } - break; - } - } - - if (index == -1) - { - return null; - } - - var mhc2Bytes = bytes.AsSpan(Range.StartAt(index)); - - return new MHC2Tag(mhc2Bytes); - } - } - - internal class ExtraInfoTag - { - public SDRTransferFunction SDRTransferFunction { get; set; } - public double Gamma { get; set; } - public double SDRMinBrightness { get; set; } - public double SDRMaxBrightness { get; set; } - public double SDRBrightnessBoost { get; set; } - public double ShadowDetailBoost { get; set; } - public ColorGamut TargetGamut { get; set; } - } - - internal class IccContext - { - protected IccProfile profile; - public CIEXYZ IlluminantRelativeWhitePoint { get; } - public Matrix? ChromaticAdaptionMatrix { get; } - public Matrix? InverseChromaticAdaptionMatrix { get; } - public RgbPrimaries ProfilePrimaries { get; } - public MHC2Tag? MHC2 { get; set; } - - public IccContext(IccProfile profile) - { - if (profile.PCS != ColorSpaceSignature.XYZ || profile.ColorSpace != ColorSpaceSignature.Rgb) - { - throw new CmsException(CmsError.COLORSPACE_CHECK, "ICC profile is not XYZ->RGB"); - } - this.profile = profile; - { - if (profile.TryReadTag(SafeTagSignature.ChromaticAdaptationTag, out var chad)) - ChromaticAdaptionMatrix = DenseMatrix.OfArray(chad); - InverseChromaticAdaptionMatrix = ChromaticAdaptionMatrix?.Inverse(); - } - (IlluminantRelativeWhitePoint, ProfilePrimaries) = PopulatePrimaries(); - - MHC2 = MHC2Tag.LoadFromProfile(profile); - } - - private unsafe CIEXYZ GetIlluminantReletiveWhitePoint() - { - if (profile.TryReadTag(SafeTagSignature.MediaWhitePointTag, out var icc_wtpt)) - { - if (ChromaticAdaptionMatrix == null || profile.HeaderCreator == 0x6170706c /* 'aapl' */) - { - // for profiels without 'chad' tag and Apple profiles, mediaWhitepointTag is illuminant-relative - return icc_wtpt; - } - else - { - // ... otherwise it is PCS-relative - var pcs_wtpt = icc_wtpt; - if (ChromaticAdaptionMatrix != null) - { - return ApplyInverseChad(pcs_wtpt); - } - } - } - if (ChromaticAdaptionMatrix != null) - { - // no wtpt in icc, sum RGB and reverse chad - var pcs_rXYZ = profile.ReadTag(SafeTagSignature.RedColorantTag); - var pcs_gXYZ = profile.ReadTag(SafeTagSignature.GreenColorantTag); - var pcs_bXYZ = profile.ReadTag(SafeTagSignature.BlueColorantTag); - var pcs_sumrgb = pcs_rXYZ + pcs_gXYZ + pcs_bXYZ; - - return ApplyInverseChad(pcs_sumrgb); - } - else - { - throw new Exception("malformed profile: missing wtpt and chad"); - } - } - - protected CIEXYZ ApplyInverseChad(in CIEXYZ val) - { - var vec = InverseChromaticAdaptionMatrix!.Multiply(new DenseVector(new[] { val.X, val.Y, val.Z })); - return new() { X = vec[0], Y = vec[1], Z = vec[2] }; - } - - - /// - /// use lcms transform to get illuminant-relative primaries. - /// - private unsafe (CIEXYZ, RgbPrimaries) PopulatePrimaries() - { - var ctx = new CmsContext(); - - ctx.SetAdaptionState(0); - - var xyzprof = IccProfile.CreateXYZ(); - var t = new CmsTransform(ctx, profile, CmsPixelFormat.RGBDouble, xyzprof, CmsPixelFormat.XYZDouble, RenderingIntent.ABSOLUTE_COLORIMETRIC, default); - var pixels = new ReadOnlySpan(new double[] { - 1, 0, 0, - 0, 1, 0, - 0, 0, 1, - 1, 1, 1 - }); - Span xyz = stackalloc double[3]; - - - t.DoTransform(pixels.Slice(0), xyz, 1); - var rXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] }; - t.DoTransform(pixels.Slice(3), xyz, 1); - var gXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] }; - t.DoTransform(pixels.Slice(6), xyz, 1); - var bXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] }; - t.DoTransform(pixels.Slice(9), xyz, 1); - var wXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] }; - - - return (wXYZ, new(rXYZ.ToXY(), gXYZ.ToXY(), bXYZ.ToXY(), wXYZ.ToXY())); - - } - - public string GetDescription() - { - return profile.GetInfo(InfoType.Description); - } - - public CIEXYZ GetIlluminantRelativeBlackPoint() - { - // NOTE: mediaBlackPointTag is no longer in ICC standard - if (profile.TryReadTag(SafeTagSignature.MediaBlackPointTag, out var bkpt)) - { - // no chad in profile, bkpt is illuminant-relative - if (ChromaticAdaptionMatrix == null) - { - return bkpt; - } - else - { - return ApplyInverseChad(bkpt); - } - } - - // no bkpt in tag, use lcms transform - var ctx = new CmsContext(); - ctx.SetAdaptionState(0); - var t = new CmsTransform(ctx, profile, CmsPixelFormat.RGB8, IccProfile.CreateXYZ(), CmsPixelFormat.XYZDouble, RenderingIntent.ABSOLUTE_COLORIMETRIC, default); - var input = new ReadOnlySpan(new byte[] { 0, 0, 0 }); - Span outbuf = stackalloc double[3]; - t.DoTransform(input, outbuf, 1); - bkpt = new CIEXYZ { X = outbuf[0], Y = outbuf[1], Z = outbuf[2] }; - return bkpt; - } - - public void WriteIlluminantRelativeMediaBlackPoint(in CIEXYZ value) - { - CIEXYZ valueToWrite; - if (ChromaticAdaptionMatrix != null) - { - var vec = new DenseVector(new double[] { value.X, value.Y, value.Z }); - var pcs_vec = ChromaticAdaptionMatrix * vec; - valueToWrite = new() { X = pcs_vec[0], Y = pcs_vec[1], Z = pcs_vec[2] }; - } - else - { - valueToWrite = value; - } - profile.WriteTag(SafeTagSignature.MediaBlackPointTag, valueToWrite); - } - - public static Matrix GetChromaticAdaptationMatrix(CIEXYZ sourceIlluminant, CIEXYZ targetIlluminant) - { - // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html - // Bradford - var M_a = DenseMatrix.OfArray(new[,] { - { 0.8951, 0.2664, -0.1614 }, - { -0.7502, 1.7135, 0.0367 }, - { 0.0389, -0.0685, 1.0296 }, - }); - - var M_a_inv = M_a.Inverse(); - - var cone_s_vec = M_a * new DenseVector(new double[] { sourceIlluminant.X, sourceIlluminant.Y, sourceIlluminant.Z }); - var cone_t_vec = M_a * new DenseVector(new double[] { targetIlluminant.X, targetIlluminant.Y, targetIlluminant.Z }); - - var M = M_a_inv * DenseMatrix.Build.DenseOfDiagonalVector(cone_t_vec / cone_s_vec) * M_a; - - return M; - } - } - - internal class DeviceIccContext : IccContext - { - CIEXYZ illuminantRelativeBlackPoint; - public double min_nits; - public double max_nits; - ToneCurve profileRedToneCurve; - ToneCurve profileGreenToneCurve; - ToneCurve profileBlueToneCurve; - ToneCurve profileRedReverseToneCurve; - ToneCurve profileGreenReverseToneCurve; - ToneCurve profileBlueReverseToneCurve; - - public ExtraInfoTag? ExtraInfoTag { get; } - - public bool UseChromaticAdaptation { get; set; } - - public DeviceIccContext(IccProfile profile) : base(profile) - { - illuminantRelativeBlackPoint = GetIlluminantRelativeBlackPoint(); - (max_nits, min_nits) = GetProfileLuminance(); - profileRedToneCurve = profile.ReadTag(SafeTagSignature.RedTRCTag); - profileGreenToneCurve = profile.ReadTag(SafeTagSignature.GreenTRCTag); - profileBlueToneCurve = profile.ReadTag(SafeTagSignature.BlueTRCTag); - profileRedReverseToneCurve = profileRedToneCurve.Reverse(); - profileGreenReverseToneCurve = profileGreenToneCurve.Reverse(); - profileBlueReverseToneCurve = profileBlueToneCurve.Reverse(); - - if (profile.ContainsTag(TagSignature.ScreeningDescTag)) - { - var ccDesc = profile.ReadTag(SafeTagSignature.ScreeningDescTag); - var json = ccDesc?.Get("en", "US"); - if (json != null) - { - ExtraInfoTag = JsonSerializer.Deserialize(json); - } - } - } - - public static Matrix RgbToXYZ(RgbPrimaries primaries) - { - var rXYZ = primaries.Red.ToXYZ(); - var gXYZ = primaries.Green.ToXYZ(); - var bXYZ = primaries.Blue.ToXYZ(); - var wXYZ = primaries.White.ToXYZ(); - - var S = DenseMatrix.OfArray(new[,] { - {rXYZ.X, gXYZ.X, bXYZ.X}, - {rXYZ.Y, gXYZ.Y, bXYZ.Y}, - {rXYZ.Z, gXYZ.Z, bXYZ.Z}, - }).Inverse().Multiply(DenseMatrix.OfArray(new[,] { { wXYZ.X }, { wXYZ.Y }, { wXYZ.Z } })); - - var M = DenseMatrix.OfArray(new[,] { - {S[0,0] * rXYZ.X, S[1,0]*gXYZ.X, S[2,0]*bXYZ.X }, - {S[0,0] * rXYZ.Y, S[1,0]*gXYZ.Y, S[2,0]*bXYZ.Y }, - {S[0,0] * rXYZ.Z, S[1,0]*gXYZ.Z, S[2,0]*bXYZ.Z }, - }); - return M; - } - - public static Matrix XYZToRgb(RgbPrimaries primaries) => RgbToXYZ(primaries).Inverse(); - - public static Matrix RgbToRgb(RgbPrimaries from, RgbPrimaries to) - { - var M1 = RgbToXYZ(from); - var M2 = XYZToRgb(to); - return M2 * M1; - } - - public string GetDeviceDescription() - { - var model = profile.GetInfo(InfoType.Model); - if (!string.IsNullOrEmpty(model)) return model; - var desc = GetDescription(); - if (!string.IsNullOrEmpty(desc)) return desc; - return ""; - } - - private (double MaxNits, double MinNits) GetProfileLuminance() - { - var wtpt = IlluminantRelativeWhitePoint; - double max_nits = 80; - if (profile.TryReadTag(SafeTagSignature.LuminanceTag, out var lumi)) - { - max_nits = lumi.Y; - } - var min_nits = 0.0; - var bkpt = illuminantRelativeBlackPoint; - if (bkpt.Y != 0) - { - var bkpt_scale = bkpt.Y / wtpt.Y; - min_nits = max_nits * bkpt_scale; - } - return (max_nits, min_nits); - } - - public IccProfile CreateMhc2CscIcc(RgbPrimaries? sourcePrimaries = null, string sourceDescription = "sRGB") - { - var wtpt = IlluminantRelativeWhitePoint; - var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray(); - - var devicePrimaries = ProfilePrimaries; - - var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve }; - - var srgbTrc = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag)!; - var sourceEotf = new ToneCurve[] { srgbTrc, srgbTrc, srgbTrc }; - - sourcePrimaries ??= RgbPrimaries.sRGB; - - var srgb_to_xyz = RgbToXYZ(RgbPrimaries.sRGB); - var xyz_to_srgb = XYZToRgb(RgbPrimaries.sRGB); - - - Matrix user_matrix = DenseMatrix.CreateIdentity(3); - - // pipeline here: input signal converted to XYZ (interpreted as sRGB) - - if (!ReferenceEquals(sourcePrimaries, RgbPrimaries.sRGB)) - { - user_matrix = RgbToXYZ(sourcePrimaries) * xyz_to_srgb * user_matrix; - } - - // pipeline here: input signal converted to XYZ (interpreted as custom RGB) - - if (UseChromaticAdaptation) - { - user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix; - } - - // pipeline here: input signal XYZ adapted to device white point - - // hook: scale white point - - var source_white_to_xyz = user_matrix * new DenseVector(new double[] { 1, 1, 1 }); - var mapped_y = source_white_to_xyz[1]; - var profile_max_nits = max_nits * (mapped_y / wtpt.Y); - - // end hook - - user_matrix = XYZToRgb(devicePrimaries) * user_matrix; - - // pipeline here: linear device RGB - - // hack: eliminate fixed sRGB to XYZ transform - - user_matrix = srgb_to_xyz * user_matrix; - - var mhc2_matrix = new double[,] { - { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, - { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, - { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, - }; - - double[,] mhc2_lut; - if (vcgt != null) - { - var lut_size = 1024; - mhc2_lut = new double[3, lut_size]; - for (int ch = 0; ch < 3; ch++) - { - for (int iinput = 0; iinput < lut_size; iinput++) - { - var input = (float)iinput / (lut_size - 1); - var linear = sourceEotf[ch].EvalF32(input); - var dev_output = deviceOetf[ch].EvalF32(linear); - if (vcgt != null) - { - dev_output = vcgt[ch].EvalF32(dev_output); - } - mhc2_lut[ch, iinput] = dev_output; - } - } - } - else - { - mhc2_lut = new double[,] - { - { 0, 1 }, - { 0, 1 }, - { 0, 1 }, - }; - } - - var mhc2d = new MHC2Tag - { - MinCLL = min_nits, - MaxCLL = profile_max_nits, - Matrix3x4 = mhc2_matrix, - RegammaLUT = mhc2_lut - }; - - var mhc2 = mhc2d.ToBytes(); - - var outputProfile = IccProfile.CreateRGB(sourcePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE - { - Red = sourcePrimaries.Red.ToXYZ().ToCIExyY(), - Green = sourcePrimaries.Green.ToXYZ().ToCIExyY(), - Blue = sourcePrimaries.Blue.ToXYZ().ToCIExyY() - }, new RgbToneCurve(srgbTrc, srgbTrc, srgbTrc)); - - outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = profile_max_nits }); - - var outctx = new IccContext(outputProfile); - outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); - - // copy device description from device profile - var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag }; - - unsafe - { - foreach (var tag in copy_tags) - { - var tag_ptr = profile.ReadTag(tag); - if (tag_ptr != null) - { - outputProfile.WriteTag(tag, tag_ptr); - } - } - } - - // set output profile description - outputProfile.HeaderManufacturer = profile.HeaderManufacturer; - outputProfile.HeaderModel = profile.HeaderModel; - outputProfile.HeaderAttributes = profile.HeaderAttributes; - outputProfile.HeaderRenderingIntent = RenderingIntent.ABSOLUTE_COLORIMETRIC; - - var new_desc = $"CSC: {sourceDescription} ({GetDeviceDescription()})"; - var new_desc_mlu = new MLU(new_desc); - outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); - - outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2); - - outputProfile.ComputeProfileId(); - - return outputProfile; - } - - public IccProfile CreatePQ10DecodeIcc(double? maxBrightnessOverride = null, double? minBrightnessOverride = null) - { - var sourcePrimaries = RgbPrimaries.Rec2020; - var devicePrimaries = ProfilePrimaries; - - Matrix user_matrix = DenseMatrix.CreateIdentity(3); - - - if (UseChromaticAdaptation) - { - user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix; - } - - - // var rgb_transform = RgbToRgb(sourcePrimaries, devicePrimaries); - // rgb_transform = XYZToRgb(devicePrimaries) * RgbToXYZ(sourcePrimaries); - // var xyz_transform = RgbToXYZ(sourcePrimaries) * rgb_transform * XYZToRgb(sourcePrimaries); - user_matrix = RgbToXYZ(sourcePrimaries) * XYZToRgb(devicePrimaries) * user_matrix; - - var mhc2_matrix = new double[,] { - { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, - { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, - { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, - }; - - var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray(); - var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve }; - - var use_max_nits = maxBrightnessOverride ?? max_nits; - var use_min_nits = minBrightnessOverride ?? min_nits; - - var lut_size = 4096; - var mhc2_lut = new double[3, 4096]; - for (int ch = 0; ch < 3; ch++) - { - for (int iinput = 0; iinput < lut_size; iinput++) - { - var pqinput = (double)iinput / (lut_size - 1); - var nits = ST2084.SignalToNits(pqinput); - var linear = Math.Max(nits - use_min_nits, 0) / (use_max_nits - use_min_nits); - var dev_output = deviceOetf[ch].EvalF32((float)linear); - if (vcgt != null) - { - dev_output = vcgt[ch].EvalF32(dev_output); - } - // Console.WriteLine($"Channel {ch}: PQ {iinput} -> {nits} cd/m2 -> SDR {dev_output * 255}"); - mhc2_lut[ch, iinput] = dev_output; - } - } - - var mhc2d = new MHC2Tag - { - MinCLL = use_min_nits, - MaxCLL = use_max_nits, - Matrix3x4 = mhc2_matrix, - RegammaLUT = mhc2_lut - }; - - var mhc2 = mhc2d.ToBytes(); - - var outputProfile = IccProfile.CreateRGB(devicePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE - { - Red = devicePrimaries.Red.ToXYZ().ToCIExyY(), - Green = devicePrimaries.Green.ToXYZ().ToCIExyY(), - Blue = devicePrimaries.Blue.ToXYZ().ToCIExyY() - }, new RgbToneCurve(profileRedToneCurve, profileGreenToneCurve, profileBlueToneCurve)); - - // copy characteristics from device profile - var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag }; - unsafe - { - foreach (var tag in copy_tags) - { - var tag_ptr = profile.ReadTag(tag); - if (tag_ptr != null) - { - outputProfile.WriteTag(tag, tag_ptr); - } - } - } - - outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = use_max_nits }); - - // the profile is not read by regular applications - // var outctx = new IccContext(outputProfile); - // outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); - - // set output profile description - outputProfile.HeaderManufacturer = profile.HeaderManufacturer; - outputProfile.HeaderModel = profile.HeaderModel; - outputProfile.HeaderAttributes = profile.HeaderAttributes; - outputProfile.HeaderRenderingIntent = RenderingIntent.ABSOLUTE_COLORIMETRIC; - - var new_desc = $"CSC: HDR10 to SDR ({GetDeviceDescription()}, {use_max_nits:0} nits)"; - var new_desc_mlu = new MLU(new_desc); - outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); - - outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2); - - outputProfile.ComputeProfileId(); - - return outputProfile; - } - - public IccProfile CreateSdrAcmIcc(bool calibrateTransfer) - { - Matrix user_matrix = DenseMatrix.CreateIdentity(3); - - if (UseChromaticAdaptation) - { - user_matrix = GetChromaticAdaptationMatrix(new CIExy { x = 0.3127, y = 0.3290 }.ToXYZ(), ProfilePrimaries.White.ToXYZ()) * user_matrix; - } - - var mhc2_matrix = new double[,] { - { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, - { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, - { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, - }; - - double[,] mhc2_lut; - - var outputprofileTrc = new RgbToneCurve(profileRedToneCurve, profileGreenToneCurve, profileBlueToneCurve); - var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray(); - - if (calibrateTransfer) - { - var sourceEotf = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag); - outputprofileTrc = new RgbToneCurve(sourceEotf, sourceEotf, sourceEotf); - - var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve }; - var lut_size = 1024; - mhc2_lut = new double[3, lut_size]; - for (int ch = 0; ch < 3; ch++) - { - for (int iinput = 0; iinput < lut_size; iinput++) - { - var input = (float)iinput / (lut_size - 1); - var linear = sourceEotf.EvalF32(input); - var dev_output = deviceOetf[ch].EvalF32(linear); - if (vcgt != null) - { - dev_output = vcgt[ch].EvalF32(dev_output); - } - mhc2_lut[ch, iinput] = dev_output; - } - - } - } - else if (vcgt != null) - { - // move vcgt to mhc2 only - var lut_size = 1024; - mhc2_lut = new double[3, lut_size]; - for (int ch = 0; ch < 3; ch++) - { - for (int iinput = 0; iinput < lut_size; iinput++) - { - var input = (float)iinput / (lut_size - 1); - var dev_output = vcgt[ch].EvalF32(input); - mhc2_lut[ch, iinput] = dev_output; - } - } - } - else - { - mhc2_lut = new double[,] { { 0, 1 }, { 0, 1 }, { 0, 1 } }; - } - - - var mhc2d = new MHC2Tag - { - MinCLL = min_nits, - MaxCLL = max_nits, - Matrix3x4 = mhc2_matrix, - RegammaLUT = mhc2_lut - }; - - var mhc2 = mhc2d.ToBytes(); - - var devicePrimaries = ProfilePrimaries; - var outputProfile = IccProfile.CreateRGB(devicePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE - { - Red = devicePrimaries.Red.ToXYZ().ToCIExyY(), - Green = devicePrimaries.Green.ToXYZ().ToCIExyY(), - Blue = devicePrimaries.Blue.ToXYZ().ToCIExyY() - }, outputprofileTrc); - - // copy characteristics from device profile - var copy_tags = new TagSignature[] { - TagSignature.LuminanceTag, - TagSignature.DeviceMfgDescTag, - TagSignature.DeviceModelDescTag, - }; - - unsafe - { - foreach (var tag in copy_tags) - { - var tag_ptr = profile.ReadTag(tag); - outputProfile.WriteTag(tag, tag_ptr); - } - } - - // the profile is not read by regular applications - //var outctx = new IccContext(outputProfile); - //outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); - - // SDR ACM will not work if the profile has negative XYZ colorants (possibly due to limited precision) - foreach (var sig in new ISafeTagSignature[] { SafeTagSignature.RedColorantTag, SafeTagSignature.GreenColorantTag, SafeTagSignature.BlueColorantTag }) - { - var xyz = outputProfile.ReadTag(sig); - if (xyz.X < 0) xyz.X = 0; - if (xyz.Y < 0) xyz.Y = 0; - if (xyz.Z < 0) xyz.Z = 0; - outputProfile.WriteTag(sig, xyz); - } - - // set output profile description - - outputProfile.HeaderManufacturer = profile.HeaderManufacturer; - outputProfile.HeaderModel = profile.HeaderModel; - outputProfile.HeaderAttributes = profile.HeaderAttributes; - outputProfile.HeaderRenderingIntent = profile.HeaderRenderingIntent; - - // outputProfile.ProfileVersion = profile.ProfileVersion; - - var new_desc = $"SDR ACM: {profile.GetInfo(InfoType.Description)}"; - var new_desc_mlu = new MLU(new_desc); - outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); - - outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2); - - outputProfile.ComputeProfileId(); - - return outputProfile; - } - - public IccProfile CreateIcc(GenerateProfileCommand command) - { - var maxNits = command.WhiteLuminance; - - //var wtpt = IlluminantRelativeWhitePoint; - - var devicePrimaries = command.DevicePrimaries; // new RgbPrimaries(new() { x = 0.698, y = 0.292 }, new() { x = 0.255, y = 0.699 }, new() { x = 0.148, y = 0.056 }, new() { x = 0.3127, y = 0.3290 }); - - var srgbTrc = profile.ReadTag(SafeTagSignature.RedTRCTag)!; - - var sourcePrimaries = RgbPrimaries.sRGB; - - var xyz_to_srgb = XYZToRgb(sourcePrimaries); - - Matrix user_matrix = DenseMatrix.CreateIdentity(3); - - // pipeline here: input signal converted to XYZ (interpreted as sRGB) - - if (command.ColorGamut != ColorGamut.Native && !ReferenceEquals(sourcePrimaries, RgbPrimaries.sRGB)) - { - user_matrix = RgbToXYZ(sourcePrimaries) * xyz_to_srgb * user_matrix; - } - - // pipeline here: input signal converted to XYZ (interpreted as custom RGB) - - if (UseChromaticAdaptation) - { - user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix; - } - - // pipeline here: input signal XYZ adapted to device white point - - // hook: scale white point - - //var source_white_to_xyz = user_matrix * new DenseVector(new double[] { 1, 1, 1 }); - //var mapped_y = source_white_to_xyz[1]; - //var profile_max_nits = max_nits * (mapped_y / wtpt.Y); - var profile_max_nits = maxNits; - - // end hook - - // pipeline here: linear device RGB - - // hack: eliminate fixed sRGB to XYZ transform - - // CSC - var targetPrimaries = command.ColorGamut switch - { - ColorGamut.Native => devicePrimaries, - ColorGamut.sRGB => RgbPrimaries.sRGB, - ColorGamut.P3 => RgbPrimaries.P3D65, - ColorGamut.Rec2020 => RgbPrimaries.Rec2020, - ColorGamut.AdobeRGB => RgbPrimaries.AdobeRGB, - _ => devicePrimaries - }; - - if (command.ColorGamut != ColorGamut.Native) - { - var srgb_to_xyz = RgbToXYZ(targetPrimaries); - - user_matrix = XYZToRgb(devicePrimaries) * user_matrix; - - user_matrix = srgb_to_xyz * user_matrix; - } - - var mhc2_matrix = new double[,] { - { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, - { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, - { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, - }; + for (var x = 0; x < 3; x++) + { + for (var y = 0; y < 4; y++) + { + Matrix3x4[x, y] = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4))); + } + } - MHC2 = new MHC2Tag - { - MinCLL = command.MinCLL, - MaxCLL = command.MaxCLL - }; + RegammaLUT = new double[3, lutSize]; - //if (!command.IsHDRProfile) - //{ - // MHC2.ApplyGamma(); - //} - if (!command.IsHDRProfile || command.SDRTransferFunction == SDRTransferFunction.Piecewise) - { - MHC2.ApplyPiecewise(command.SDRBrightnessBoost); - } - else if (command.SDRTransferFunction == SDRTransferFunction.PurePower) - { - MHC2.ApplySdrAcm(command.SDRMaxBrightness, command.SDRMinBrightness, command.Gamma, command.SDRBrightnessBoost, command.ShadowDetailBoost); - } - else if (command.SDRTransferFunction == SDRTransferFunction.BT_1886) - { - MHC2.ApplySdrAcm(120, 0.03, 2.4, command.SDRBrightnessBoost, command.ShadowDetailBoost); - } + for (var c = 0; c < 3; c++) + { + var discard = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(8)); - MHC2.Matrix3x4 = mhc2_matrix; + for (var x = 0; x < lutSize; x++) + { + RegammaLUT[c, x] = DecodeS15F16(BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4))); + } + } + } + + public void ApplySdrAcm(double whiteLuminance = 120.0, double blackLuminance = 0.0, double gamma = 2.2, double boostPercentage = 0, double shadowDetailBoost = 0) + { + var lutSize = 1024; + var amplifier = boostPercentage == 0 ? 1 : 1 + boostPercentage / 100; + + RegammaLUT = new double[3, lutSize]; + + for (var i = 0; i < lutSize; i++) + { + var (_, value) = CmsFunctions.SrgbAcm(i, whiteLuminance, blackLuminance, gamma, lutSize - 1); + + // Average between sSRGB and Piecewise gamma + if (shadowDetailBoost > 0) + { + var piecewiseValue = (double)i / 1023; + + var correctedBoost = (shadowDetailBoost * (1023 - i)) / 1023; + + value = ((value * (100 - correctedBoost)) + (piecewiseValue * correctedBoost)) / 100; + } + + for (var c = 0; c < 3; c++) + { + //var tempPercentage = boostPercentage * ((double)i / lutSize); + + //var amplifier = tempPercentage == 0 ? 1 : tempPercentage > 0 ? 1 + tempPercentage / 100 : 1 - tempPercentage / 100; + + RegammaLUT[c, i] = value * amplifier; + } + } + } + + public void ApplyPiecewise(double boostPercentage = 0) + { + var lutSize = 1024; + var amplifier = boostPercentage == 0 ? 1 : 1 + boostPercentage / 100; + + RegammaLUT = new double[3, lutSize]; + + for (var i = 0; i < lutSize; i++) + { + var value = (double)i / 1023; + + for (var c = 0; c < 3; c++) + { + RegammaLUT[c, i] = value * amplifier; + } + } + } + + public void ApplyGamma(double gamma = 2.2, double shadowDetailBoost = 0) + { + var lutSize = 1024; + + RegammaLUT = new double[3, lutSize]; + + for (var i = 0; i < lutSize; i++) + { + var value = CmsFunctions.RgbToLinear((double)i / (lutSize - 1), gamma); + + // Average between sSRGB and Piecewise gamma + if (shadowDetailBoost > 0) + { + var piecewiseValue = (double)i / lutSize; + + var correctedBoost = (shadowDetailBoost * (lutSize - i)) / lutSize; + + value = ((value * (100 - correctedBoost)) + (piecewiseValue * correctedBoost)) / 100; + } + + for (var c = 0; c < 3; c++) + { + RegammaLUT[c, i] = value; + } + } + } + + private static int EncodeS15F16(double value) + { + var x = (int)Math.Round(value * 65536); + return x; + } + + private static double DecodeS15F16(int value) + { + return (double)value / 65536; + } + + public byte[] ToBytes() + { + if (RegammaLUT!.GetLength(0) != 3) throw new ArrayTypeMismatchException(); + var lut_size = RegammaLUT.GetLength(1); + if (lut_size <= 1 || lut_size > 4096) throw new IndexOutOfRangeException(); + if (Matrix3x4!.Length != 12 || Matrix3x4.GetLength(0) != 3) throw new ArrayTypeMismatchException(); + var ms0 = new MemoryStream(); + var writer = new BinaryWriter(ms0); + // type + writer.Write(BinaryPrimitives.ReverseEndianness((int)Signature)); + writer.Write(BinaryPrimitives.ReverseEndianness(0)); + + writer.Write(BinaryPrimitives.ReverseEndianness(lut_size)); + writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(MinCLL))); + writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(MaxCLL))); + // matrix offset + writer.Write(BinaryPrimitives.ReverseEndianness(36)); + // lut0 offset + writer.Write(BinaryPrimitives.ReverseEndianness(84)); + // lut1 offset + var lut1_offset = 84 + 8 + lut_size * 4; + writer.Write(BinaryPrimitives.ReverseEndianness(lut1_offset)); + // lut2 offset + var lut2_offset = lut1_offset + 8 + lut_size * 4; + writer.Write(BinaryPrimitives.ReverseEndianness(lut2_offset)); + + foreach (var e in Matrix3x4) + { + writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(e))); + } + + for (int ch = 0; ch < 3; ch++) + { + writer.Write(new ReadOnlySpan(new byte[] { (byte)'s', (byte)'f', (byte)'3', (byte)'2', 0, 0, 0, 0 })); + for (int i = 0; i < lut_size; i++) + { + writer.Write(BinaryPrimitives.ReverseEndianness(EncodeS15F16(RegammaLUT[ch, i]))); + } + } + writer.Flush(); + return ms0.ToArray(); + } + + internal static MHC2Tag? LoadFromProfile(IccProfile profile) + { + var bytes = profile.RawBytes; + + if (bytes == null) + { + return null; + } + + // 0x4D484332 + var index = -1; + var first = true; + for (var i = 0; i < bytes.Length; i += 4) + { + var chunk = bytes.AsSpan(new Range(i, i + 4)); + + if (chunk[0] == 77 && chunk[1] == 72 && chunk[2] == 67 && chunk[3] == 50) + { + index = i; + + if (first) + { + index = -1; + first = false; + continue; + } + break; + } + } + + if (index == -1) + { + return null; + } + + var mhc2Bytes = bytes.AsSpan(Range.StartAt(index)); + + return new MHC2Tag(mhc2Bytes); + } + } + + internal class ExtraInfoTag + { + public SDRTransferFunction SDRTransferFunction { get; set; } + public double Gamma { get; set; } + public double SDRMinBrightness { get; set; } + public double SDRMaxBrightness { get; set; } + public double SDRBrightnessBoost { get; set; } + public double ShadowDetailBoost { get; set; } + public ColorGamut TargetGamut { get; set; } + } + + internal class IccContext + { + protected IccProfile profile; + public CIEXYZ IlluminantRelativeWhitePoint { get; } + public Matrix? ChromaticAdaptionMatrix { get; } + public Matrix? InverseChromaticAdaptionMatrix { get; } + public RgbPrimaries ProfilePrimaries { get; } + public MHC2Tag? MHC2 { get; set; } + + public IccContext(IccProfile profile) + { + if (profile.PCS != ColorSpaceSignature.XYZ || profile.ColorSpace != ColorSpaceSignature.Rgb) + { + throw new CmsException(CmsError.COLORSPACE_CHECK, "ICC profile is not XYZ->RGB"); + } + this.profile = profile; + { + if (profile.TryReadTag(SafeTagSignature.ChromaticAdaptationTag, out var chad)) + ChromaticAdaptionMatrix = DenseMatrix.OfArray(chad); + InverseChromaticAdaptionMatrix = ChromaticAdaptionMatrix?.Inverse(); + } + (IlluminantRelativeWhitePoint, ProfilePrimaries) = PopulatePrimaries(); + + MHC2 = MHC2Tag.LoadFromProfile(profile); + } + + private unsafe CIEXYZ GetIlluminantReletiveWhitePoint() + { + if (profile.TryReadTag(SafeTagSignature.MediaWhitePointTag, out var icc_wtpt)) + { + if (ChromaticAdaptionMatrix == null || profile.HeaderCreator == 0x6170706c /* 'aapl' */) + { + // for profiels without 'chad' tag and Apple profiles, mediaWhitepointTag is illuminant-relative + return icc_wtpt; + } + else + { + // ... otherwise it is PCS-relative + var pcs_wtpt = icc_wtpt; + if (ChromaticAdaptionMatrix != null) + { + return ApplyInverseChad(pcs_wtpt); + } + } + } + if (ChromaticAdaptionMatrix != null) + { + // no wtpt in icc, sum RGB and reverse chad + var pcs_rXYZ = profile.ReadTag(SafeTagSignature.RedColorantTag); + var pcs_gXYZ = profile.ReadTag(SafeTagSignature.GreenColorantTag); + var pcs_bXYZ = profile.ReadTag(SafeTagSignature.BlueColorantTag); + var pcs_sumrgb = pcs_rXYZ + pcs_gXYZ + pcs_bXYZ; + + return ApplyInverseChad(pcs_sumrgb); + } + else + { + throw new Exception("malformed profile: missing wtpt and chad"); + } + } + + protected CIEXYZ ApplyInverseChad(in CIEXYZ val) + { + var vec = InverseChromaticAdaptionMatrix!.Multiply(new DenseVector(new[] { val.X, val.Y, val.Z })); + return new() { X = vec[0], Y = vec[1], Z = vec[2] }; + } + + + /// + /// use lcms transform to get illuminant-relative primaries. + /// + private unsafe (CIEXYZ, RgbPrimaries) PopulatePrimaries() + { + var ctx = new CmsContext(); + + ctx.SetAdaptionState(0); + + var xyzprof = IccProfile.CreateXYZ(); + var t = new CmsTransform(ctx, profile, CmsPixelFormat.RGBDouble, xyzprof, CmsPixelFormat.XYZDouble, RenderingIntent.ABSOLUTE_COLORIMETRIC, default); + var pixels = new ReadOnlySpan(new double[] { + 1, 0, 0, + 0, 1, 0, + 0, 0, 1, + 1, 1, 1 + }); + Span xyz = stackalloc double[3]; + + + t.DoTransform(pixels.Slice(0), xyz, 1); + var rXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] }; + t.DoTransform(pixels.Slice(3), xyz, 1); + var gXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] }; + t.DoTransform(pixels.Slice(6), xyz, 1); + var bXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] }; + t.DoTransform(pixels.Slice(9), xyz, 1); + var wXYZ = new CIEXYZ { X = xyz[0], Y = xyz[1], Z = xyz[2] }; + + + return (wXYZ, new(rXYZ.ToXY(), gXYZ.ToXY(), bXYZ.ToXY(), wXYZ.ToXY())); + + } + + public string GetDescription() + { + return profile.GetInfo(InfoType.Description); + } + + public CIEXYZ GetIlluminantRelativeBlackPoint() + { + // NOTE: mediaBlackPointTag is no longer in ICC standard + if (profile.TryReadTag(SafeTagSignature.MediaBlackPointTag, out var bkpt)) + { + // no chad in profile, bkpt is illuminant-relative + if (ChromaticAdaptionMatrix == null) + { + return bkpt; + } + else + { + return ApplyInverseChad(bkpt); + } + } + + // no bkpt in tag, use lcms transform + var ctx = new CmsContext(); + ctx.SetAdaptionState(0); + var t = new CmsTransform(ctx, profile, CmsPixelFormat.RGB8, IccProfile.CreateXYZ(), CmsPixelFormat.XYZDouble, RenderingIntent.ABSOLUTE_COLORIMETRIC, default); + var input = new ReadOnlySpan(new byte[] { 0, 0, 0 }); + Span outbuf = stackalloc double[3]; + t.DoTransform(input, outbuf, 1); + bkpt = new CIEXYZ { X = outbuf[0], Y = outbuf[1], Z = outbuf[2] }; + return bkpt; + } + + public void WriteIlluminantRelativeMediaBlackPoint(in CIEXYZ value) + { + CIEXYZ valueToWrite; + if (ChromaticAdaptionMatrix != null) + { + var vec = new DenseVector(new double[] { value.X, value.Y, value.Z }); + var pcs_vec = ChromaticAdaptionMatrix * vec; + valueToWrite = new() { X = pcs_vec[0], Y = pcs_vec[1], Z = pcs_vec[2] }; + } + else + { + valueToWrite = value; + } + profile.WriteTag(SafeTagSignature.MediaBlackPointTag, valueToWrite); + } + + public static Matrix GetChromaticAdaptationMatrix(CIEXYZ sourceIlluminant, CIEXYZ targetIlluminant) + { + // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html + // Bradford + var M_a = DenseMatrix.OfArray(new[,] { + { 0.8951, 0.2664, -0.1614 }, + { -0.7502, 1.7135, 0.0367 }, + { 0.0389, -0.0685, 1.0296 }, + }); + + var M_a_inv = M_a.Inverse(); + + var cone_s_vec = M_a * new DenseVector(new double[] { sourceIlluminant.X, sourceIlluminant.Y, sourceIlluminant.Z }); + var cone_t_vec = M_a * new DenseVector(new double[] { targetIlluminant.X, targetIlluminant.Y, targetIlluminant.Z }); + + var M = M_a_inv * DenseMatrix.Build.DenseOfDiagonalVector(cone_t_vec / cone_s_vec) * M_a; + + return M; + } + } + + internal class DeviceIccContext : IccContext + { + CIEXYZ illuminantRelativeBlackPoint; + public double min_nits; + public double max_nits; + ToneCurve profileRedToneCurve; + ToneCurve profileGreenToneCurve; + ToneCurve profileBlueToneCurve; + ToneCurve profileRedReverseToneCurve; + ToneCurve profileGreenReverseToneCurve; + ToneCurve profileBlueReverseToneCurve; + + public ExtraInfoTag? ExtraInfoTag { get; } + + public bool UseChromaticAdaptation { get; set; } + + public DeviceIccContext(IccProfile profile) : base(profile) + { + illuminantRelativeBlackPoint = GetIlluminantRelativeBlackPoint(); + (max_nits, min_nits) = GetProfileLuminance(); + profileRedToneCurve = profile.ReadTag(SafeTagSignature.RedTRCTag); + profileGreenToneCurve = profile.ReadTag(SafeTagSignature.GreenTRCTag); + profileBlueToneCurve = profile.ReadTag(SafeTagSignature.BlueTRCTag); + profileRedReverseToneCurve = profileRedToneCurve.Reverse(); + profileGreenReverseToneCurve = profileGreenToneCurve.Reverse(); + profileBlueReverseToneCurve = profileBlueToneCurve.Reverse(); + + if (profile.ContainsTag(TagSignature.ScreeningDescTag)) + { + var ccDesc = profile.ReadTag(SafeTagSignature.ScreeningDescTag); + var json = ccDesc?.Get("en", "US"); + if (json != null) + { + ExtraInfoTag = JsonSerializer.Deserialize(json); + } + } + } + + public static Matrix RgbToXYZ(RgbPrimaries primaries) + { + var rXYZ = primaries.Red.ToXYZ(); + var gXYZ = primaries.Green.ToXYZ(); + var bXYZ = primaries.Blue.ToXYZ(); + var wXYZ = primaries.White.ToXYZ(); + + var S = DenseMatrix.OfArray(new[,] { + {rXYZ.X, gXYZ.X, bXYZ.X}, + {rXYZ.Y, gXYZ.Y, bXYZ.Y}, + {rXYZ.Z, gXYZ.Z, bXYZ.Z}, + }).Inverse().Multiply(DenseMatrix.OfArray(new[,] { { wXYZ.X }, { wXYZ.Y }, { wXYZ.Z } })); + + var M = DenseMatrix.OfArray(new[,] { + {S[0,0] * rXYZ.X, S[1,0]*gXYZ.X, S[2,0]*bXYZ.X }, + {S[0,0] * rXYZ.Y, S[1,0]*gXYZ.Y, S[2,0]*bXYZ.Y }, + {S[0,0] * rXYZ.Z, S[1,0]*gXYZ.Z, S[2,0]*bXYZ.Z }, + }); + return M; + } + + public static Matrix XYZToRgb(RgbPrimaries primaries) => RgbToXYZ(primaries).Inverse(); + + public static Matrix RgbToRgb(RgbPrimaries from, RgbPrimaries to) + { + var M1 = RgbToXYZ(from); + var M2 = XYZToRgb(to); + return M2 * M1; + } + + public string GetDeviceDescription() + { + var model = profile.GetInfo(InfoType.Model); + if (!string.IsNullOrEmpty(model)) return model; + var desc = GetDescription(); + if (!string.IsNullOrEmpty(desc)) return desc; + return ""; + } + + private (double MaxNits, double MinNits) GetProfileLuminance() + { + var wtpt = IlluminantRelativeWhitePoint; + double max_nits = 80; + if (profile.TryReadTag(SafeTagSignature.LuminanceTag, out var lumi)) + { + max_nits = lumi.Y; + } + var min_nits = 0.0; + var bkpt = illuminantRelativeBlackPoint; + if (bkpt.Y != 0) + { + var bkpt_scale = bkpt.Y / wtpt.Y; + min_nits = max_nits * bkpt_scale; + } + return (max_nits, min_nits); + } + + public IccProfile CreateMhc2CscIcc(RgbPrimaries? sourcePrimaries = null, string sourceDescription = "sRGB") + { + var wtpt = IlluminantRelativeWhitePoint; + var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray(); + + var devicePrimaries = ProfilePrimaries; + + var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve }; + + var srgbTrc = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag)!; + var sourceEotf = new ToneCurve[] { srgbTrc, srgbTrc, srgbTrc }; + + sourcePrimaries ??= RgbPrimaries.sRGB; + + var srgb_to_xyz = RgbToXYZ(RgbPrimaries.sRGB); + var xyz_to_srgb = XYZToRgb(RgbPrimaries.sRGB); + + + Matrix user_matrix = DenseMatrix.CreateIdentity(3); + + // pipeline here: input signal converted to XYZ (interpreted as sRGB) + + if (!ReferenceEquals(sourcePrimaries, RgbPrimaries.sRGB)) + { + user_matrix = RgbToXYZ(sourcePrimaries) * xyz_to_srgb * user_matrix; + } + + // pipeline here: input signal converted to XYZ (interpreted as custom RGB) + + if (UseChromaticAdaptation) + { + user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix; + } + + // pipeline here: input signal XYZ adapted to device white point + + // hook: scale white point + + var source_white_to_xyz = user_matrix * new DenseVector(new double[] { 1, 1, 1 }); + var mapped_y = source_white_to_xyz[1]; + var profile_max_nits = max_nits * (mapped_y / wtpt.Y); + + // end hook + + user_matrix = XYZToRgb(devicePrimaries) * user_matrix; + + // pipeline here: linear device RGB + + // hack: eliminate fixed sRGB to XYZ transform + + user_matrix = srgb_to_xyz * user_matrix; + + var mhc2_matrix = new double[,] { + { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, + { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, + { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, + }; + + double[,] mhc2_lut; + if (vcgt != null) + { + var lut_size = 1024; + mhc2_lut = new double[3, lut_size]; + for (int ch = 0; ch < 3; ch++) + { + for (int iinput = 0; iinput < lut_size; iinput++) + { + var input = (float)iinput / (lut_size - 1); + var linear = sourceEotf[ch].EvalF32(input); + var dev_output = deviceOetf[ch].EvalF32(linear); + if (vcgt != null) + { + dev_output = vcgt[ch].EvalF32(dev_output); + } + mhc2_lut[ch, iinput] = dev_output; + } + } + } + else + { + mhc2_lut = new double[,] + { + { 0, 1 }, + { 0, 1 }, + { 0, 1 }, + }; + } + + var mhc2d = new MHC2Tag + { + MinCLL = min_nits, + MaxCLL = profile_max_nits, + Matrix3x4 = mhc2_matrix, + RegammaLUT = mhc2_lut + }; + + var mhc2 = mhc2d.ToBytes(); + + var outputProfile = IccProfile.CreateRGB(sourcePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE + { + Red = sourcePrimaries.Red.ToXYZ().ToCIExyY(), + Green = sourcePrimaries.Green.ToXYZ().ToCIExyY(), + Blue = sourcePrimaries.Blue.ToXYZ().ToCIExyY() + }, new RgbToneCurve(srgbTrc, srgbTrc, srgbTrc)); + + outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = profile_max_nits }); + + var outctx = new IccContext(outputProfile); + outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); + + // copy device description from device profile + var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag }; + + unsafe + { + foreach (var tag in copy_tags) + { + var tag_ptr = profile.ReadTag(tag); + if (tag_ptr != null) + { + outputProfile.WriteTag(tag, tag_ptr); + } + } + } + + // set output profile description + outputProfile.HeaderManufacturer = profile.HeaderManufacturer; + outputProfile.HeaderModel = profile.HeaderModel; + outputProfile.HeaderAttributes = profile.HeaderAttributes; + outputProfile.HeaderRenderingIntent = RenderingIntent.ABSOLUTE_COLORIMETRIC; + + var new_desc = $"CSC: {sourceDescription} ({GetDeviceDescription()})"; + var new_desc_mlu = new MLU(new_desc); + outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); + + outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2); + + outputProfile.ComputeProfileId(); + + return outputProfile; + } + + public IccProfile CreatePQ10DecodeIcc(double? maxBrightnessOverride = null, double? minBrightnessOverride = null) + { + var sourcePrimaries = RgbPrimaries.Rec2020; + var devicePrimaries = ProfilePrimaries; + + Matrix user_matrix = DenseMatrix.CreateIdentity(3); + + + if (UseChromaticAdaptation) + { + user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix; + } + + + // var rgb_transform = RgbToRgb(sourcePrimaries, devicePrimaries); + // rgb_transform = XYZToRgb(devicePrimaries) * RgbToXYZ(sourcePrimaries); + // var xyz_transform = RgbToXYZ(sourcePrimaries) * rgb_transform * XYZToRgb(sourcePrimaries); + user_matrix = RgbToXYZ(sourcePrimaries) * XYZToRgb(devicePrimaries) * user_matrix; + + var mhc2_matrix = new double[,] { + { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, + { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, + { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, + }; + + var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray(); + var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve }; + + var use_max_nits = maxBrightnessOverride ?? max_nits; + var use_min_nits = minBrightnessOverride ?? min_nits; + + var lut_size = 4096; + var mhc2_lut = new double[3, 4096]; + for (int ch = 0; ch < 3; ch++) + { + for (int iinput = 0; iinput < lut_size; iinput++) + { + var pqinput = (double)iinput / (lut_size - 1); + var nits = ST2084.SignalToNits(pqinput); + var linear = Math.Max(nits - use_min_nits, 0) / (use_max_nits - use_min_nits); + var dev_output = deviceOetf[ch].EvalF32((float)linear); + if (vcgt != null) + { + dev_output = vcgt[ch].EvalF32(dev_output); + } + // Console.WriteLine($"Channel {ch}: PQ {iinput} -> {nits} cd/m2 -> SDR {dev_output * 255}"); + mhc2_lut[ch, iinput] = dev_output; + } + } + + var mhc2d = new MHC2Tag + { + MinCLL = use_min_nits, + MaxCLL = use_max_nits, + Matrix3x4 = mhc2_matrix, + RegammaLUT = mhc2_lut + }; + + var mhc2 = mhc2d.ToBytes(); + + var outputProfile = IccProfile.CreateRGB(devicePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE + { + Red = devicePrimaries.Red.ToXYZ().ToCIExyY(), + Green = devicePrimaries.Green.ToXYZ().ToCIExyY(), + Blue = devicePrimaries.Blue.ToXYZ().ToCIExyY() + }, new RgbToneCurve(profileRedToneCurve, profileGreenToneCurve, profileBlueToneCurve)); + + // copy characteristics from device profile + var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag }; + unsafe + { + foreach (var tag in copy_tags) + { + var tag_ptr = profile.ReadTag(tag); + if (tag_ptr != null) + { + outputProfile.WriteTag(tag, tag_ptr); + } + } + } + + outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = use_max_nits }); + + // the profile is not read by regular applications + // var outctx = new IccContext(outputProfile); + // outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); + + // set output profile description + outputProfile.HeaderManufacturer = profile.HeaderManufacturer; + outputProfile.HeaderModel = profile.HeaderModel; + outputProfile.HeaderAttributes = profile.HeaderAttributes; + outputProfile.HeaderRenderingIntent = RenderingIntent.ABSOLUTE_COLORIMETRIC; + + var new_desc = $"CSC: HDR10 to SDR ({GetDeviceDescription()}, {use_max_nits:0} nits)"; + var new_desc_mlu = new MLU(new_desc); + outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); + + outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2); + + outputProfile.ComputeProfileId(); + + return outputProfile; + } + + public IccProfile CreateSdrAcmIcc(bool calibrateTransfer) + { + Matrix user_matrix = DenseMatrix.CreateIdentity(3); + + if (UseChromaticAdaptation) + { + user_matrix = GetChromaticAdaptationMatrix(new CIExy { x = 0.3127, y = 0.3290 }.ToXYZ(), ProfilePrimaries.White.ToXYZ()) * user_matrix; + } + + var mhc2_matrix = new double[,] { + { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, + { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, + { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, + }; + + double[,] mhc2_lut; + + var outputprofileTrc = new RgbToneCurve(profileRedToneCurve, profileGreenToneCurve, profileBlueToneCurve); + var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray(); + + if (calibrateTransfer) + { + var sourceEotf = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag); + outputprofileTrc = new RgbToneCurve(sourceEotf, sourceEotf, sourceEotf); + + var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve }; + var lut_size = 1024; + mhc2_lut = new double[3, lut_size]; + for (int ch = 0; ch < 3; ch++) + { + for (int iinput = 0; iinput < lut_size; iinput++) + { + var input = (float)iinput / (lut_size - 1); + var linear = sourceEotf.EvalF32(input); + var dev_output = deviceOetf[ch].EvalF32(linear); + if (vcgt != null) + { + dev_output = vcgt[ch].EvalF32(dev_output); + } + mhc2_lut[ch, iinput] = dev_output; + } + + } + } + else if (vcgt != null) + { + // move vcgt to mhc2 only + var lut_size = 1024; + mhc2_lut = new double[3, lut_size]; + for (int ch = 0; ch < 3; ch++) + { + for (int iinput = 0; iinput < lut_size; iinput++) + { + var input = (float)iinput / (lut_size - 1); + var dev_output = vcgt[ch].EvalF32(input); + mhc2_lut[ch, iinput] = dev_output; + } + } + } + else + { + mhc2_lut = new double[,] { { 0, 1 }, { 0, 1 }, { 0, 1 } }; + } + + + var mhc2d = new MHC2Tag + { + MinCLL = min_nits, + MaxCLL = max_nits, + Matrix3x4 = mhc2_matrix, + RegammaLUT = mhc2_lut + }; + + var mhc2 = mhc2d.ToBytes(); + + var devicePrimaries = ProfilePrimaries; + var outputProfile = IccProfile.CreateRGB(devicePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE + { + Red = devicePrimaries.Red.ToXYZ().ToCIExyY(), + Green = devicePrimaries.Green.ToXYZ().ToCIExyY(), + Blue = devicePrimaries.Blue.ToXYZ().ToCIExyY() + }, outputprofileTrc); + + // copy characteristics from device profile + var copy_tags = new TagSignature[] { + TagSignature.LuminanceTag, + TagSignature.DeviceMfgDescTag, + TagSignature.DeviceModelDescTag, + }; + + unsafe + { + foreach (var tag in copy_tags) + { + var tag_ptr = profile.ReadTag(tag); + outputProfile.WriteTag(tag, tag_ptr); + } + } + + // the profile is not read by regular applications + //var outctx = new IccContext(outputProfile); + //outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); + + // SDR ACM will not work if the profile has negative XYZ colorants (possibly due to limited precision) + foreach (var sig in new ISafeTagSignature[] { SafeTagSignature.RedColorantTag, SafeTagSignature.GreenColorantTag, SafeTagSignature.BlueColorantTag }) + { + var xyz = outputProfile.ReadTag(sig); + if (xyz.X < 0) xyz.X = 0; + if (xyz.Y < 0) xyz.Y = 0; + if (xyz.Z < 0) xyz.Z = 0; + outputProfile.WriteTag(sig, xyz); + } + + // set output profile description + + outputProfile.HeaderManufacturer = profile.HeaderManufacturer; + outputProfile.HeaderModel = profile.HeaderModel; + outputProfile.HeaderAttributes = profile.HeaderAttributes; + outputProfile.HeaderRenderingIntent = profile.HeaderRenderingIntent; + + // outputProfile.ProfileVersion = profile.ProfileVersion; + + var new_desc = $"SDR ACM: {profile.GetInfo(InfoType.Description)}"; + var new_desc_mlu = new MLU(new_desc); + outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); + + outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2); + + outputProfile.ComputeProfileId(); + + return outputProfile; + } + + public IccProfile CreateIcc(GenerateProfileCommand command) + { + var maxNits = command.WhiteLuminance; + + //var wtpt = IlluminantRelativeWhitePoint; + + var devicePrimaries = command.DevicePrimaries; // new RgbPrimaries(new() { x = 0.698, y = 0.292 }, new() { x = 0.255, y = 0.699 }, new() { x = 0.148, y = 0.056 }, new() { x = 0.3127, y = 0.3290 }); + + var srgbTrc = profile.ReadTag(SafeTagSignature.RedTRCTag)!; + + var sourcePrimaries = RgbPrimaries.sRGB; + + var xyz_to_srgb = XYZToRgb(sourcePrimaries); + + Matrix user_matrix = DenseMatrix.CreateIdentity(3); + + // pipeline here: input signal converted to XYZ (interpreted as sRGB) + + if (command.ColorGamut != ColorGamut.Native && !ReferenceEquals(sourcePrimaries, RgbPrimaries.sRGB)) + { + user_matrix = RgbToXYZ(sourcePrimaries) * xyz_to_srgb * user_matrix; + } + + // pipeline here: input signal converted to XYZ (interpreted as custom RGB) + + if (UseChromaticAdaptation) + { + user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix; + } + + // pipeline here: input signal XYZ adapted to device white point + + // hook: scale white point + + //var source_white_to_xyz = user_matrix * new DenseVector(new double[] { 1, 1, 1 }); + //var mapped_y = source_white_to_xyz[1]; + //var profile_max_nits = max_nits * (mapped_y / wtpt.Y); + var profile_max_nits = maxNits; + + // end hook + + // pipeline here: linear device RGB + + // hack: eliminate fixed sRGB to XYZ transform + + // CSC + var targetPrimaries = command.ColorGamut switch + { + ColorGamut.Native => devicePrimaries, + ColorGamut.sRGB => RgbPrimaries.sRGB, + ColorGamut.P3 => RgbPrimaries.P3D65, + ColorGamut.Rec2020 => RgbPrimaries.Rec2020, + ColorGamut.AdobeRGB => RgbPrimaries.AdobeRGB, + _ => devicePrimaries + }; + + if (command.ColorGamut != ColorGamut.Native) + { + var srgb_to_xyz = RgbToXYZ(targetPrimaries); + + user_matrix = XYZToRgb(devicePrimaries) * user_matrix; + + user_matrix = srgb_to_xyz * user_matrix; + } + + var mhc2_matrix = new double[,] { + { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, + { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, + { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, + }; + + MHC2 = new MHC2Tag + { + MinCLL = command.MinCLL, + MaxCLL = command.MaxCLL + }; + + if (command.IsHDRProfile) + { + if (command.SDRTransferFunction == SDRTransferFunction.Piecewise) + { + MHC2.ApplyPiecewise(command.SDRBrightnessBoost); + } + else if (command.SDRTransferFunction == SDRTransferFunction.PurePower) + { + MHC2.ApplySdrAcm(command.SDRMaxBrightness, command.SDRMinBrightness, command.Gamma, command.SDRBrightnessBoost, command.ShadowDetailBoost); + } + else if (command.SDRTransferFunction == SDRTransferFunction.BT_1886) + { + MHC2.ApplySdrAcm(120, 0.03, 2.4, command.SDRBrightnessBoost, command.ShadowDetailBoost); + } + } + else + { + if (command.SDRTransferFunction == SDRTransferFunction.PurePower) + { + MHC2.ApplyGamma(command.Gamma, command.ShadowDetailBoost); + } + else + { + MHC2.ApplyPiecewise(command.SDRBrightnessBoost); + } + } - var outputProfile = IccProfile.CreateRGB(devicePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE - { - Red = devicePrimaries.Red.ToXYZ().ToCIExyY(), - Green = devicePrimaries.Green.ToXYZ().ToCIExyY(), - Blue = devicePrimaries.Blue.ToXYZ().ToCIExyY() - }, new RgbToneCurve(srgbTrc, srgbTrc, srgbTrc)); + MHC2.Matrix3x4 = mhc2_matrix; - outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = profile_max_nits }); + var outputProfile = IccProfile.CreateRGB(devicePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE + { + Red = devicePrimaries.Red.ToXYZ().ToCIExyY(), + Green = devicePrimaries.Green.ToXYZ().ToCIExyY(), + Blue = devicePrimaries.Blue.ToXYZ().ToCIExyY() + }, new RgbToneCurve(srgbTrc, srgbTrc, srgbTrc)); + + outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = profile_max_nits }); + + var outctx = new IccContext(outputProfile); + outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); + + // copy device description from device profile + var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag }; + + unsafe + { + foreach (var tag in copy_tags) + { + var tag_ptr = profile.ReadTag(tag); + if (tag_ptr != null) + { + outputProfile.WriteTag(tag, tag_ptr); + } + } + } + + // set output profile description + outputProfile.HeaderManufacturer = profile.HeaderManufacturer; + outputProfile.HeaderModel = profile.HeaderModel; + outputProfile.HeaderAttributes = profile.HeaderAttributes; + outputProfile.HeaderRenderingIntent = RenderingIntent.PERCEPTUAL; + + var descAppendix = $" ({GetDeviceDescription()})"; + var new_desc = command.Description ?? ""; + if (!new_desc.Contains(descAppendix)) + { + new_desc += descAppendix; + } + var new_desc_mlu = new MLU(new_desc); + outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); + + var extraInfoTag = new ExtraInfoTag + { + SDRTransferFunction = command.SDRTransferFunction, + Gamma = command.Gamma, + SDRMinBrightness = command.SDRMinBrightness, + SDRMaxBrightness = command.SDRMaxBrightness, + SDRBrightnessBoost = command.SDRBrightnessBoost, + ShadowDetailBoost = command.ShadowDetailBoost, + TargetGamut = command.ColorGamut + }; + + var ccDesc = JsonSerializer.Serialize(extraInfoTag); + + outputProfile.WriteTag(SafeTagSignature.ScreeningDescTag, new MLU(ccDesc)); + + outputProfile.WriteRawTag(MHC2Tag.Signature, MHC2.ToBytes()); + + outputProfile.ComputeProfileId(); + + return outputProfile; + } + + public IccProfile CreateCscIcc(RgbPrimaries? sourcePrimaries = null, string sourceDescription = "sRGB") + { + var wtpt = IlluminantRelativeWhitePoint; + var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray(); + + var customPrimaries = new RgbPrimaries(new() { x = 0.698, y = 0.292 }, new() { x = 0.255, y = 0.699 }, new() { x = 0.148, y = 0.056 }, new() { x = 0.3127, y = 0.3290 }); + + var devicePrimaries = customPrimaries; + + var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve }; + + var srgbTrc = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag)!; + var sourceEotf = new ToneCurve[] { srgbTrc, srgbTrc, srgbTrc }; + + sourcePrimaries ??= RgbPrimaries.sRGB; + + var srgb_to_xyz = RgbToXYZ(RgbPrimaries.sRGB); + var xyz_to_srgb = XYZToRgb(RgbPrimaries.sRGB); + + + Matrix user_matrix = DenseMatrix.CreateIdentity(3); + + // pipeline here: input signal converted to XYZ (interpreted as sRGB) + + if (!ReferenceEquals(sourcePrimaries, RgbPrimaries.sRGB)) + { + user_matrix = RgbToXYZ(sourcePrimaries) * xyz_to_srgb * user_matrix; + } + + // pipeline here: input signal converted to XYZ (interpreted as custom RGB) + + if (UseChromaticAdaptation) + { + user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix; + } + + // pipeline here: input signal XYZ adapted to device white point + + // hook: scale white point + + var source_white_to_xyz = user_matrix * new DenseVector(new double[] { 1, 1, 1 }); + var mapped_y = source_white_to_xyz[1]; + var profile_max_nits = max_nits * (mapped_y / wtpt.Y); + + // end hook + + user_matrix = XYZToRgb(devicePrimaries) * user_matrix; + + // pipeline here: linear device RGB + + // hack: eliminate fixed sRGB to XYZ transform + + user_matrix = srgb_to_xyz * user_matrix; + + var mhc2_matrix = new double[,] { + { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, + { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, + { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, + }; + + double[,] mhc2_lut; + if (vcgt != null) + { + var lut_size = 1024; + mhc2_lut = new double[3, lut_size]; + for (int ch = 0; ch < 3; ch++) + { + for (int iinput = 0; iinput < lut_size; iinput++) + { + var input = (float)iinput / (lut_size - 1); + var linear = sourceEotf[ch].EvalF32(input); + var dev_output = deviceOetf[ch].EvalF32(linear); + if (vcgt != null) + { + dev_output = vcgt[ch].EvalF32(dev_output); + } + mhc2_lut[ch, iinput] = dev_output; + } + } + } + else + { + var lut_size = 1024; + mhc2_lut = new double[3, lut_size]; + for (int ch = 0; ch < 3; ch++) + { + for (int iinput = 0; iinput < lut_size; iinput++) + { + var input = (float)iinput / (lut_size - 1); + var linear = sourceEotf[ch].EvalF32(input); - var outctx = new IccContext(outputProfile); - outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); + var dev_output = CmsFunctions.RgbToLinear(linear, 1.6); - // copy device description from device profile - var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag }; - - unsafe - { - foreach (var tag in copy_tags) - { - var tag_ptr = profile.ReadTag(tag); - if (tag_ptr != null) - { - outputProfile.WriteTag(tag, tag_ptr); - } - } - } - - // set output profile description - outputProfile.HeaderManufacturer = profile.HeaderManufacturer; - outputProfile.HeaderModel = profile.HeaderModel; - outputProfile.HeaderAttributes = profile.HeaderAttributes; - outputProfile.HeaderRenderingIntent = RenderingIntent.PERCEPTUAL; - - var descAppendix = $" ({GetDeviceDescription()})"; - var new_desc = command.Description ?? ""; - if (!new_desc.Contains(descAppendix)) - { - new_desc += descAppendix; - } - var new_desc_mlu = new MLU(new_desc); - outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); - - var extraInfoTag = new ExtraInfoTag - { - SDRTransferFunction = command.SDRTransferFunction, - Gamma = command.Gamma, - SDRMinBrightness = command.SDRMinBrightness, - SDRMaxBrightness = command.SDRMaxBrightness, - SDRBrightnessBoost = command.SDRBrightnessBoost, - ShadowDetailBoost = command.ShadowDetailBoost, - TargetGamut = command.ColorGamut - }; - - var ccDesc = JsonSerializer.Serialize(extraInfoTag); - - outputProfile.WriteTag(SafeTagSignature.ScreeningDescTag, new MLU(ccDesc)); - - outputProfile.WriteRawTag(MHC2Tag.Signature, MHC2.ToBytes()); - - outputProfile.ComputeProfileId(); - - return outputProfile; - } - - public IccProfile CreateCscIcc(RgbPrimaries? sourcePrimaries = null, string sourceDescription = "sRGB") - { - var wtpt = IlluminantRelativeWhitePoint; - var vcgt = profile.ReadTagOrDefault(SafeTagSignature.VcgtTag)?.ToArray(); - - var customPrimaries = new RgbPrimaries(new() { x = 0.698, y = 0.292 }, new() { x = 0.255, y = 0.699 }, new() { x = 0.148, y = 0.056 }, new() { x = 0.3127, y = 0.3290 }); - - var devicePrimaries = customPrimaries; - - var deviceOetf = new ToneCurve[] { profileRedReverseToneCurve, profileGreenReverseToneCurve, profileBlueReverseToneCurve }; - - var srgbTrc = IccProfile.Create_sRGB().ReadTag(SafeTagSignature.RedTRCTag)!; - var sourceEotf = new ToneCurve[] { srgbTrc, srgbTrc, srgbTrc }; - - sourcePrimaries ??= RgbPrimaries.sRGB; - - var srgb_to_xyz = RgbToXYZ(RgbPrimaries.sRGB); - var xyz_to_srgb = XYZToRgb(RgbPrimaries.sRGB); - - - Matrix user_matrix = DenseMatrix.CreateIdentity(3); - - // pipeline here: input signal converted to XYZ (interpreted as sRGB) - - if (!ReferenceEquals(sourcePrimaries, RgbPrimaries.sRGB)) - { - user_matrix = RgbToXYZ(sourcePrimaries) * xyz_to_srgb * user_matrix; - } - - // pipeline here: input signal converted to XYZ (interpreted as custom RGB) - - if (UseChromaticAdaptation) - { - user_matrix = GetChromaticAdaptationMatrix(sourcePrimaries.White.ToXYZ(), devicePrimaries.White.ToXYZ()) * user_matrix; - } - - // pipeline here: input signal XYZ adapted to device white point - - // hook: scale white point - - var source_white_to_xyz = user_matrix * new DenseVector(new double[] { 1, 1, 1 }); - var mapped_y = source_white_to_xyz[1]; - var profile_max_nits = max_nits * (mapped_y / wtpt.Y); - - // end hook - - user_matrix = XYZToRgb(devicePrimaries) * user_matrix; - - // pipeline here: linear device RGB - - // hack: eliminate fixed sRGB to XYZ transform - - user_matrix = srgb_to_xyz * user_matrix; - - var mhc2_matrix = new double[,] { - { user_matrix[0,0], user_matrix[0,1], user_matrix[0,2], 0 }, - { user_matrix[1,0], user_matrix[1,1], user_matrix[1,2], 0 }, - { user_matrix[2,0], user_matrix[2,1], user_matrix[2,2], 0 }, - }; - - double[,] mhc2_lut; - if (vcgt != null) - { - var lut_size = 1024; - mhc2_lut = new double[3, lut_size]; - for (int ch = 0; ch < 3; ch++) - { - for (int iinput = 0; iinput < lut_size; iinput++) - { - var input = (float)iinput / (lut_size - 1); - var linear = sourceEotf[ch].EvalF32(input); - var dev_output = deviceOetf[ch].EvalF32(linear); - if (vcgt != null) - { - dev_output = vcgt[ch].EvalF32(dev_output); - } - mhc2_lut[ch, iinput] = dev_output; - } - } - } - else - { - var lut_size = 1024; - mhc2_lut = new double[3, lut_size]; - for (int ch = 0; ch < 3; ch++) - { - for (int iinput = 0; iinput < lut_size; iinput++) - { - var input = (float)iinput / (lut_size - 1); - var linear = sourceEotf[ch].EvalF32(input); - - var dev_output = CmsFunctions.RgbToLinear(linear, 1.6); - - //var dev_output = deviceOetf[ch].EvalF32(input); - //if (vcgt != null) - //{ - // dev_output = vcgt[ch].EvalF32(dev_output); - //} - mhc2_lut[ch, iinput] = dev_output; - } - } - } - - var mhc2d = new MHC2Tag - { - MinCLL = min_nits, - MaxCLL = profile_max_nits, - Matrix3x4 = mhc2_matrix, - RegammaLUT = mhc2_lut - }; - - var mhc2 = mhc2d.ToBytes(); - - var outputProfile = IccProfile.CreateRGB(sourcePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE - { - Red = sourcePrimaries.Red.ToXYZ().ToCIExyY(), - Green = sourcePrimaries.Green.ToXYZ().ToCIExyY(), - Blue = sourcePrimaries.Blue.ToXYZ().ToCIExyY() - }, new RgbToneCurve(srgbTrc, srgbTrc, srgbTrc)); - - outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = profile_max_nits }); - - var outctx = new IccContext(outputProfile); - outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); - - // copy device description from device profile - var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag }; - - unsafe - { - foreach (var tag in copy_tags) - { - var tag_ptr = profile.ReadTag(tag); - if (tag_ptr != null) - { - outputProfile.WriteTag(tag, tag_ptr); - } - } - } - - // set output profile description - outputProfile.HeaderManufacturer = profile.HeaderManufacturer; - outputProfile.HeaderModel = profile.HeaderModel; - outputProfile.HeaderAttributes = profile.HeaderAttributes; - outputProfile.HeaderRenderingIntent = RenderingIntent.PERCEPTUAL; - - var new_desc = $"XCSC: {sourceDescription} ({GetDeviceDescription()})"; - var new_desc_mlu = new MLU(new_desc); - outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); - - outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2); - - outputProfile.ComputeProfileId(); - - return outputProfile; - } - - } + //var dev_output = deviceOetf[ch].EvalF32(input); + //if (vcgt != null) + //{ + // dev_output = vcgt[ch].EvalF32(dev_output); + //} + mhc2_lut[ch, iinput] = dev_output; + } + } + } + + var mhc2d = new MHC2Tag + { + MinCLL = min_nits, + MaxCLL = profile_max_nits, + Matrix3x4 = mhc2_matrix, + RegammaLUT = mhc2_lut + }; + + var mhc2 = mhc2d.ToBytes(); + + var outputProfile = IccProfile.CreateRGB(sourcePrimaries.White.ToXYZ().ToCIExyY(), new CIExyYTRIPLE + { + Red = sourcePrimaries.Red.ToXYZ().ToCIExyY(), + Green = sourcePrimaries.Green.ToXYZ().ToCIExyY(), + Blue = sourcePrimaries.Blue.ToXYZ().ToCIExyY() + }, new RgbToneCurve(srgbTrc, srgbTrc, srgbTrc)); + + outputProfile.WriteTag(SafeTagSignature.LuminanceTag, new CIEXYZ { Y = profile_max_nits }); + + var outctx = new IccContext(outputProfile); + outctx.WriteIlluminantRelativeMediaBlackPoint(illuminantRelativeBlackPoint); + + // copy device description from device profile + var copy_tags = new TagSignature[] { TagSignature.DeviceMfgDescTag, TagSignature.DeviceModelDescTag }; + + unsafe + { + foreach (var tag in copy_tags) + { + var tag_ptr = profile.ReadTag(tag); + if (tag_ptr != null) + { + outputProfile.WriteTag(tag, tag_ptr); + } + } + } + + // set output profile description + outputProfile.HeaderManufacturer = profile.HeaderManufacturer; + outputProfile.HeaderModel = profile.HeaderModel; + outputProfile.HeaderAttributes = profile.HeaderAttributes; + outputProfile.HeaderRenderingIntent = RenderingIntent.PERCEPTUAL; + + var new_desc = $"XCSC: {sourceDescription} ({GetDeviceDescription()})"; + var new_desc_mlu = new MLU(new_desc); + outputProfile.WriteTag(SafeTagSignature.ProfileDescriptionTag, new_desc_mlu); + + outputProfile.WriteRawTag(MHC2Tag.Signature, mhc2); + + outputProfile.ComputeProfileId(); + + return outputProfile; + } + + } } diff --git a/MHC2Gen/MHC2Gen.csproj b/MHC2Gen/MHC2Gen.csproj index 3bc7ca6..6791b28 100644 --- a/MHC2Gen/MHC2Gen.csproj +++ b/MHC2Gen/MHC2Gen.csproj @@ -1,21 +1,16 @@  - net8.0 + net9.0-windows enable - true x64 + true - - net8.0 - true - - true diff --git a/NLogViewer/NLogViewer.csproj b/NLogViewer/NLogViewer.csproj index 5e3b66a..dc80ac5 100644 --- a/NLogViewer/NLogViewer.csproj +++ b/NLogViewer/NLogViewer.csproj @@ -1,7 +1,7 @@  - net8.0-windows10.0.20348.0 + net9.0-windows true DJ true @@ -18,8 +18,8 @@ - - + + diff --git a/Nspector/Common/Meta/DriverSettingMetaService.cs b/Nspector/Common/Meta/DriverSettingMetaService.cs index 2a4c046..353d6a0 100644 --- a/Nspector/Common/Meta/DriverSettingMetaService.cs +++ b/Nspector/Common/Meta/DriverSettingMetaService.cs @@ -27,8 +27,11 @@ namespace nspector.Common.Meta return settingIds; } - private SettingMeta GetDriverSettingMetaInternal(uint settingId) + private SettingMeta? GetDriverSettingMetaInternal(uint settingId) { + if ((settingId & 0xFFFFF000) == 0x10c7d000) + return null; + var values = new NVDRS_SETTING_VALUES(); values.version = nvw.NVDRS_SETTING_VALUES_VER; uint valueCount = 255; diff --git a/Nspector/Nspector.csproj b/Nspector/Nspector.csproj index 8392e5e..fb7908c 100644 --- a/Nspector/Nspector.csproj +++ b/Nspector/Nspector.csproj @@ -1,7 +1,7 @@  - net8.0-windows10.0.20348.0 + net9.0-windows enable true enable @@ -14,7 +14,7 @@ - + diff --git a/Nspector/nspector/CustomSettingNames.xml b/Nspector/nspector/CustomSettingNames.xml index cb85f92..aae9789 100644 --- a/Nspector/nspector/CustomSettingNames.xml +++ b/Nspector/nspector/CustomSettingNames.xml @@ -7913,6 +7913,27 @@ + + DLSS 3.1.11+ - Forced DLSS3.1 preset letter (base profile only) + 0x10E41DF3 + 8 - Extra + 0 + + + Preset E + 0x00000005 + + + Preset F + 0x00000006 + + + Preset J + 0x0000000A + + + + diff --git a/NvAPIWrapper/NvAPIWrapper.csproj b/NvAPIWrapper/NvAPIWrapper.csproj index bbc8f94..80f9483 100644 --- a/NvAPIWrapper/NvAPIWrapper.csproj +++ b/NvAPIWrapper/NvAPIWrapper.csproj @@ -1,7 +1,7 @@  - net8.0-windows10.0.20348.0 + net9.0-windows 0.8.1.106 falahati.net NvAPIWrapper is a .Net wrapper for NVIDIA public API diff --git a/Shared/Common/StreamString.cs b/Shared/Common/StreamString.cs index 837c440..d5d73ac 100644 --- a/Shared/Common/StreamString.cs +++ b/Shared/Common/StreamString.cs @@ -28,7 +28,7 @@ namespace ColorControl.Shared.Common var len = BitConverter.ToInt32(lenBytes); var inBuffer = new byte[len]; - ioStream.Read(inBuffer, 0, len); + ioStream.ReadExactly(inBuffer, 0, len); return streamEncoding.GetString(inBuffer); } @@ -47,7 +47,7 @@ namespace ColorControl.Shared.Common var len = BitConverter.ToInt32(lenBytes); var inBuffer = new byte[len]; - ioStream.Read(inBuffer, 0, len); + ioStream.ReadExactly(inBuffer, 0, len); return streamEncoding.GetString(inBuffer); } diff --git a/Shared/Common/Utils.cs b/Shared/Common/Utils.cs index 0f46470..5de5723 100644 --- a/Shared/Common/Utils.cs +++ b/Shared/Common/Utils.cs @@ -8,34 +8,34 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; +using System.Management; using System.Net.Http; using System.Net.NetworkInformation; using System.Reflection; using System.Security.Cryptography; using System.Text; -using Windows.Devices.Enumeration; -using Windows.Devices.Enumeration.Pnp; using Task = System.Threading.Tasks.Task; namespace ColorControl.Shared.Common { - public static class Utils - { - public const int WM_BRINGTOFRONT = NativeConstants.WM_USER + 1; + public static class Utils + { + public const int WM_BRINGTOFRONT = NativeConstants.WM_USER + 1; - public static string PKEY_PNPX_IpAddress = "{656a3bb3-ecc0-43fd-8477-4ae0404a96cd} 12297"; - public static string PKEY_PNPX_PhysicalAddress = "{656a3bb3-ecc0-43fd-8477-4ae0404a96cd} 12294"; - public static string PKEY_DEVICE_CATEGORY = "{78c34fc8-104a-4aca-9ea4-524d52996e57} 90"; - public static string DEV_CLASS_DISPLAY_TV_LCD = "Display.TV.LCD"; - public static Guid GUID_CONSOLE_DISPLAY_STATE = Guid.Parse("6FE69556-704A-47A0-8F24-C28D936FDA47"); + public static string PKEY_PNPX_IpAddress = "{656a3bb3-ecc0-43fd-8477-4ae0404a96cd} 12297"; + public static string PKEY_PNPX_PhysicalAddress = "{656a3bb3-ecc0-43fd-8477-4ae0404a96cd} 12294"; + public static string PKEY_DEVICE_CATEGORY = "{78c34fc8-104a-4aca-9ea4-524d52996e57} 90"; + public static string PKEY_DeviceContainer_Category = "DEVPKEY_DeviceContainer_Category"; + public static string DEV_CLASS_DISPLAY_TV_LCD = "Display.TV.LCD"; + public static Guid GUID_CONSOLE_DISPLAY_STATE = Guid.Parse("6FE69556-704A-47A0-8F24-C28D936FDA47"); - public static bool ConsoleOpened { get; private set; } + public static bool ConsoleOpened { get; private set; } - private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); - public static string SERVICE_NAME = "Color Control Service"; + public static string SERVICE_NAME = "Color Control Service"; - public static string ELEVATION_MSG = @"Elevation is needed in some cases where ColorControl needs administrator rights. + public static string ELEVATION_MSG = @"Elevation is needed in some cases where ColorControl needs administrator rights. Some operations like installing a service, changing the priority of a process or creating a temporary IP-route for improved WOL-functionality will not work without those rights. These methods are available: @@ -50,724 +50,717 @@ These methods are available: The best and suggested method to provide this is via a Windows Service. Only when installing the service a User Account Control window may popup."; - public static Bitmap SubPixelShift(Bitmap bitmap) - { - Bitmap bitmap2 = (Bitmap)bitmap.Clone(); - - int shift = -1; - bool shiftRed = true; - bool shiftGreen = true; - bool shiftBlue = false; - - for (int i = 0; i < bitmap.Width; i++) - { - for (int j = 0; j < bitmap.Height; j++) - { - Color pixel = bitmap.GetPixel(i, j); - - if (shift > 0 && i < bitmap.Width - shift) - { - Color pixel2 = bitmap2.GetPixel(i + shift, j); - - Color pixel3 = Color.FromArgb(shiftRed ? pixel.R : pixel2.R, shiftGreen ? pixel.G : pixel2.G, shiftBlue ? pixel.B : pixel2.B); - - bitmap2.SetPixel(i + shift, j, pixel3); - - } - else if (i + shift >= 0) - { - Color pixel2 = bitmap2.GetPixel(i + shift, j); - - Color pixel3 = Color.FromArgb(shiftRed ? pixel.R : pixel2.R, shiftGreen ? pixel.G : pixel2.G, shiftBlue ? pixel.B : pixel2.B); - - bitmap2.SetPixel(i + shift, j, pixel3); - - } - } - } - - return bitmap2; - //bitmap2.Save("d:\\ss_shifted.bmp"); - } - - public static string GetDataPath(string subDir = "") - { - var path = Directory.GetParent(Application.UserAppDataPath).FullName; - if (!string.IsNullOrEmpty(subDir)) - { - path = Path.Combine(path, subDir); - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - } - return path; - } - - public static string[] GetDeviceProperty(PnpObject device, string propertyName) - { - object objValue; - string[] values = null; - - //foreach (var key in device.Properties.Keys) - //{ - // Debug.WriteLine(key); - //} - - if (device.Properties.TryGetValue(propertyName, out objValue)) - { - if (objValue is string[] strValues) - { - values = strValues.Where(s => !s.IsNullOrWhiteSpace()).ToArray(); - } - else if (objValue is byte[]) - { - values = new[] { BitConverter.ToString((byte[])objValue) }; - } - else - { - values = new[] { objValue?.ToString() }; - } - } - - return values; - } - - public static async Task> GetPnpDevices(string deviceName, string category = null) - { - var devices = new List(); - - var list = await DeviceInformation.FindAllAsync("", new List()); - - var reqs = new List - { - PKEY_PNPX_IpAddress, - PKEY_PNPX_PhysicalAddress, - PKEY_DEVICE_CATEGORY - }; - - foreach (var dev in list) - { - string name = dev.Name; - if (/*dev.IsEnabled &&*/ dev.Name.Contains(deviceName, StringComparison.OrdinalIgnoreCase)) - { - var guid = dev.Properties["System.Devices.DeviceInstanceId"]; - - var aqs = "System.Devices.DeviceInstanceId:=\"" + guid + "\""; - var list2 = await PnpObject.FindAllAsync(PnpObjectType.Device, reqs, aqs); - - foreach (var dev2 in list2) - { - var devCategories = GetDeviceProperty(dev2, PKEY_DEVICE_CATEGORY); - - if (category != null && devCategories?.Any(c => c == category) == false) - { - continue; - } - - var ipAddresses = GetDeviceProperty(dev2, "System.Devices.IpAddress"); - var macAddresses = GetDeviceProperty(dev2, PKEY_PNPX_PhysicalAddress); - - var ipAddress = ipAddresses?.FirstOrDefault(); - var macAddress = macAddresses?.FirstOrDefault(); - - if (!devices.Any(x => x.Name.Equals(name) && x.IpAddress != null && x.IpAddress.Equals(ipAddress))) - { - var device = new PnpDev(dev, dev2, name, ipAddress, macAddress); - devices.Add(device); - } - } - - } - } - return devices; - } - - public static Image GenerateGradientBitmap(int width, int height) - { - var bitmap = new Bitmap(512, height); - - for (var i = 0; i < 256; i++) - { - var pixel = Color.FromArgb(i, i, i); - - for (var h = 0; h < height; h++) - { - bitmap.SetPixel(i * 2, h, pixel); - bitmap.SetPixel(i * 2 + 1, h, pixel); - } - } - - return bitmap; - } - - public static string FirstCharUpperCase(string name) - { - return name.Substring(0, 1).ToUpper() + name.Substring(1); - } - - public static Dictionary EnumToDictionary(IEnumerable skipValues = null) where T : struct, Enum - { - return Enum.GetValues().Where(v => skipValues == null || !skipValues.Contains(v)).ToDictionary(k => k, e => e.GetDescription()); - } - - public static string GetDescriptionByEnumName(string value) where T : IConvertible - { - string description = null; - - if (Enum.TryParse(typeof(T), value, out var result)) - { - description = (result as IConvertible)?.GetDescription(); - } - - description = description ?? FirstCharUpperCase(value); - - return description; - } - - public static string GetDescription(this T e) where T : IConvertible - { - return GetDescription(e.GetType(), e); - } - - public static string GetDescription(Type enumType, IConvertible value) - { - if (enumType.BaseType == typeof(Enum)) - { - var val = value.ToInt32(CultureInfo.InvariantCulture); - - var enumName = enumType.GetEnumName(val); - if (enumName == null) - { - return string.Empty; - } - - var memInfo = enumType.GetMember(enumName); - var descriptionAttribute = memInfo[0] - .GetCustomAttributes(typeof(DescriptionAttribute), false) - .FirstOrDefault() as DescriptionAttribute; - - if (descriptionAttribute != null) - { - return descriptionAttribute.Description; - } - - return enumName; - } - - return null; // could also return string.Empty - } - - public static T GetEnumValueByDescription(string value) where T : struct - { - var enumName = GetEnumNameByDescription(typeof(T), value); - if (enumName != null) - { - value = enumName; - } - - return Enum.Parse(value); - } - - public static string RemoveFirstUnderscore(this string text) - { - if (string.IsNullOrEmpty(text)) - { - return text; - } - - if (text[0] == '_') - { - return text.Substring(1); - } - - return text; - } - - public static string GetEnumNameByDescription(Type enumType, string description) - { - if (enumType.BaseType == typeof(Enum)) - { - foreach (var enumValue in Enum.GetValues(enumType)) - { - if (description.Equals(GetDescription(enumType, enumValue as IConvertible), StringComparison.Ordinal)) - { - return Enum.GetName(enumType, enumValue).RemoveFirstUnderscore(); - } - } - } - - return null; // could also return string.Empty - } - - public static List GetDescriptions(int value = -1, int fromValue = 0) where T : IConvertible - { - return GetDescriptions(typeof(T), value, fromValue); - } - - public static List GetDescriptions(Type enumType, int value = -1, int fromValue = 0, bool replaceUnderscore = false) - { - var list = new List(); - foreach (var enumValue in Enum.GetValues(enumType)) - { - if (value > -1 || fromValue > 0) - { - var enumIntValue = Convert.ToInt32(enumValue); - - if (enumIntValue < fromValue || value >= 0 && (enumIntValue & value) == 0) - { - continue; - } - } - - list.Add(GetDescription(enumType, enumValue as IConvertible) ?? (replaceUnderscore ? enumValue.ToString().Replace("_", "") : enumValue.ToString())); - } - return list; - } - - public static bool OpenConsole() - { - if (ConsoleOpened) - { - return true; - } - - if (!WinApi.AttachConsole(-1)) - { - WinApi.AllocConsole(); - - SetConsoleWriter(); - - return true; - } - ConsoleOpened = true; - - SetConsoleWriter(); - - return false; - } - - private static void SetConsoleWriter() - { - var writer = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }; - Console.SetOut(writer); - } - - public static bool CloseConsole() - { - if (!ConsoleOpened) - { - return false; - } - - SendKeys.SendWait("{ENTER}"); - var result = WinApi.FreeConsole(); - - return result; - } - - public static Process GetProcessByName(string name, bool skipCurrent = true) - { - var currentProcessId = Environment.ProcessId; - foreach (var process in Process.GetProcesses()) - { - if (process.ProcessName.Equals(name) && (!skipCurrent || process.Id != currentProcessId)) - { - return process; - } - } - - return null; - } - - public static string GetResourceFile(string resourceName) - { - resourceName = "ColorControl.Resources." + resourceName; - - var assembly = Assembly.GetEntryAssembly(); - - using (Stream stream = assembly.GetManifestResourceStream(resourceName)) - using (StreamReader reader = new StreamReader(stream)) - { - var result = reader.ReadToEnd(); - - return result; - } - } - - public static void ParseWords(List target, string text) - { - target.Clear(); - - if (string.IsNullOrEmpty(text)) - { - return; - } - var words = text.Split(','); - for (var i = 0; i < words.Length; i++) - { - var word = words[i]; - if (word.IndexOf("(") > -1) - { - words[i] = word.Trim(); - } - else - { - words[i] = word.Replace(" ", string.Empty); - } - } - target.AddRange(words); - } - - public static int ParseInt(string value, int def = 0) - { - if (int.TryParse(value, out var result)) - { - return result; - } - - return def; - } - - public static uint ParseUInt(string value, uint def = 0) - { - if (uint.TryParse(value, out var result)) - { - return result; - } - - return def; - } - - public static int ParseDynamicAsInt(dynamic value, int def = 0) - { - if (value.Type == JTokenType.Integer) - { - return (int)value.Value; - } - if (value.Type == JTokenType.String) - { - return ParseInt(value.Value, def); - } - return def; - } - - public static async Task GetRestJsonAsync(string url) - { - var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("request"); - - var response = await httpClient.GetAsync(url); - if (response.IsSuccessStatusCode) - { - var result = await response.Content.ReadAsStringAsync(); - - var data = JsonConvert.DeserializeObject(result); - - return data; - } - - return null; - } - - public static FileInfo SelectFile(string ext = "*.exe", string filter = "Application EXE Name|*.exe|Application Absolute Path|*.exe") - { - var openDialog = new System.Windows.Forms.OpenFileDialog(); - openDialog.DefaultExt = ext; - openDialog.Filter = filter; - - if (openDialog.ShowDialog() == DialogResult.OK) - { - return new FileInfo(openDialog.FileName); - //if (openDialog.FilterIndex == 2) - // applicationName = openDialog.FileName; - - //return applicationName; - } - - return null; - } - - public static bool NormEquals(this string str1, string str2) - { - return str1.Trim().Replace(" ", string.Empty).Equals(str2.Trim().Replace(" ", string.Empty), StringComparison.OrdinalIgnoreCase); - } - - public static async Task PingHost(string nameOrAddress) - { - try - { - using var pinger = new Ping(); - var reply = await pinger.SendPingAsync(nameOrAddress, 2000); - - return reply.Status == IPStatus.Success; - } - catch (PingException) - { - // Discard PingExceptions and return false; - return false; - } - } - - public static bool WriteText(string fileName, string data) - { - var doWrite = !File.Exists(fileName); - - if (!doWrite) - { - var writtenData = File.ReadAllText(fileName); - - doWrite = !writtenData.Equals(data); - } - - if (doWrite) - { - try - { - File.WriteAllText(fileName, data); - } - catch (Exception e) - { - Logger.Error(e.ToLogString()); - } - } - - return doWrite; - } - - public static string ReadText(string fileName, bool reverse = false) - { - var exists = File.Exists(fileName); - - if (!exists) - { - return null; - } - - try - { - var lines = new StringBuilder(); - - using var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - - using var reader = new StreamReader(fs); - - string line; - - while ((line = reader.ReadLine()) != null) - { - if (reverse) - { - lines.Insert(0, line + "\r\n"); - continue; - } - - lines.AppendLine(line); - } - - return lines.ToString(); - } - catch (Exception e) - { - Logger.Error(e.ToLogString()); - } - - return null; - } - - public static List ReadLines(string fileName, bool reverse = false) - { - var exists = File.Exists(fileName); - - if (!exists) - { - return null; - } - - try - { - var lines = new List(); - - using var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - - using var reader = new StreamReader(fs); - - string line; - - while ((line = reader.ReadLine()) != null) - { - if (reverse) - { - lines.Insert(0, line + "\r\n"); - continue; - } - - lines.Add(line); - } - - return lines; - } - catch (Exception e) - { - Logger.Error(e.ToLogString()); - } - - return null; - } - - public static bool WriteObject(string fileName, object value) - { - var data = JsonConvert.SerializeObject(value); - - return WriteText(fileName, data); - } - - public static T DeserializeJson(string fileName) - { - try - { - var json = File.ReadAllText(fileName); - var data = JsonConvert.DeserializeObject(json); - - return data; - } - catch (Exception e) - { - Logger.Error(e.ToLogString()); - - return default; - } - } - - public static string Base64Encode(string plainText) - { - var plainTextBytes = Encoding.UTF8.GetBytes(plainText); - return Convert.ToBase64String(plainTextBytes); - } - - public static string Base64Decode(string base64EncodedData) - { - var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); - return Encoding.UTF8.GetString(base64EncodedBytes); - } - - public static async Task DownloadFileAsync(string url, string filePath) - { - var httpClient = new HttpClient(); - using var stream = await httpClient.GetStreamAsync(url); - - if (File.Exists(filePath)) - { - File.Delete(filePath); - } - - using var fileStream = new FileStream(filePath, FileMode.CreateNew); - - await stream.CopyToAsync(fileStream); - } - - public static void UnZipFile(string zipFile, string filePath) - { - ZipFile.ExtractToDirectory(zipFile, filePath); - } - - public static void UpdateFiles(string clientPath, string updatePath) - { - var infos = new List(); - - GetFileSystemInfos(updatePath, infos); - - foreach (var info in infos) - { - var subPath = info.FullName.Replace(updatePath, ""); - - if (subPath[0] == '\\') - { - subPath = subPath.Substring(1); - } - - var targetPath = Path.Combine(clientPath, subPath); - - //Logger.Debug($"Path: {updatePath}, info: {info.FullName}, target: {targetPath}"); - - if (info is DirectoryInfo) - { - if (!Directory.Exists(targetPath)) - { - Directory.CreateDirectory(targetPath); - } - - continue; - } - - var fileInfo = info as FileInfo; - - var oldPath = targetPath + ".old"; - - if (File.Exists(oldPath)) - { - File.Delete(oldPath); - } - - if (File.Exists(targetPath)) - { - if (CompareFiles(fileInfo.FullName, targetPath)) - { - continue; - } - - File.Move(targetPath, targetPath + ".old"); - } - - fileInfo.CopyTo(targetPath); - } - } - - public static void GetFileSystemInfos(string path, List infos) - { - var directory = new DirectoryInfo(path); - - foreach (var dir in directory.GetDirectories()) - { - infos.Add(dir); - GetFileSystemInfos(dir.FullName, infos); - } - - foreach (var file in directory.GetFiles()) - { - infos.Add(file); - } - } - - public static string SHA256CheckSum(string filePath) - { - using (SHA256 SHA256 = SHA256.Create()) - { - using (FileStream fileStream = File.OpenRead(filePath)) - return Convert.ToBase64String(SHA256.ComputeHash(fileStream)); - } - } - - public static bool CompareFiles(string filePath1, string filePath2) - { - return SHA256CheckSum(filePath1) == SHA256CheckSum(filePath2); - } - - public static string ToKiloUnitString(this uint value, int div = 1000, string units = "MHz") => $"{value / div}{units}"; - public static string ToKiloUnitString(this int value, int div = 1000, string units = "MHz") => $"{value / div}{units}"; - public static string ToUnitString(this uint value, string units = "MHz") => $"{value}{units}"; - public static string ToSignedUnitString(this int value, string units = "MHz") => $"{(value >= 0 ? "+" + value : value)}{units}"; - public static string ToSignedUnitString(this uint value, string units = "MHz") => $"{(value >= 0 ? "+" + value : value)}{units}"; - public static string ToUnitString(string units = "°", params double?[] values) - { - var notNullValues = values.Where(v => v.HasValue).Select(v => $"{v}{units}"); - - return string.Join("/", notNullValues); - } - - public static System.Windows.Application EnsureApplication() - { - return System.Windows.Application.Current ?? new System.Windows.Application { ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown }; - } - } + public static Bitmap SubPixelShift(Bitmap bitmap) + { + Bitmap bitmap2 = (Bitmap)bitmap.Clone(); + + int shift = -1; + bool shiftRed = true; + bool shiftGreen = true; + bool shiftBlue = false; + + for (int i = 0; i < bitmap.Width; i++) + { + for (int j = 0; j < bitmap.Height; j++) + { + Color pixel = bitmap.GetPixel(i, j); + + if (shift > 0 && i < bitmap.Width - shift) + { + Color pixel2 = bitmap2.GetPixel(i + shift, j); + + Color pixel3 = Color.FromArgb(shiftRed ? pixel.R : pixel2.R, shiftGreen ? pixel.G : pixel2.G, shiftBlue ? pixel.B : pixel2.B); + + bitmap2.SetPixel(i + shift, j, pixel3); + + } + else if (i + shift >= 0) + { + Color pixel2 = bitmap2.GetPixel(i + shift, j); + + Color pixel3 = Color.FromArgb(shiftRed ? pixel.R : pixel2.R, shiftGreen ? pixel.G : pixel2.G, shiftBlue ? pixel.B : pixel2.B); + + bitmap2.SetPixel(i + shift, j, pixel3); + + } + } + } + + return bitmap2; + //bitmap2.Save("d:\\ss_shifted.bmp"); + } + + public static string GetDataPath(string subDir = "") + { + var path = Directory.GetParent(Application.UserAppDataPath).FullName; + if (!string.IsNullOrEmpty(subDir)) + { + path = Path.Combine(path, subDir); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + return path; + } + + public static string[] GetDeviceProperty(ManagementObject device, string propertyName) + { + string[] values = null; + + var args = new object[] { new string[] { propertyName }, null }; + device.InvokeMethod("GetDeviceProperties", args); + + // one mbo for each device property key + var mbos = (ManagementBaseObject[])args[1]; + if (mbos.Length > 0) + { + // get value of property named "Data" + // not all objects have that so we enum all props here + var data = mbos[0].Properties.OfType().FirstOrDefault(p => p.Name == "Data")?.Value; + if (data != null) + { + if (data is string[] strValues) + { + values = strValues.Where(s => !s.IsNullOrWhiteSpace()).ToArray(); + } + else if (data is byte[]) + { + values = [BitConverter.ToString((byte[])data)]; + } + else + { + values = [data?.ToString()]; + } + } + } + + return values; + } + + public static async Task> GetPnpDevices(string deviceName, string category = null) + { + var devices = new List(); + + var queryString = $"SELECT * FROM Win32_PnPEntity WHERE PNPClass='DigitalMediaDevices' AND NAME LIKE '%{deviceName}%'"; + using ManagementObjectSearcher mos = new ManagementObjectSearcher(queryString); + + foreach (var mo in mos.Get().OfType()) + { + //foreach (var prop in mo.Properties) + //{ + // Debug.WriteLine($"{prop.Name}: {prop.Value}"); + //} + + if (category != null) + { + var devCategory = GetDeviceProperty(mo, PKEY_DeviceContainer_Category); + + if (devCategory != null && !devCategory.Contains(category)) + { + continue; + } + } + + var name = mo["Name"] as string; + var ipAddress = GetDeviceProperty(mo, PKEY_PNPX_IpAddress)?.FirstOrDefault(); + var macAddress = GetDeviceProperty(mo, PKEY_PNPX_PhysicalAddress)?.FirstOrDefault(); + + if (!devices.Any(x => x.Name.Equals(name) && x.IpAddress != null && x.IpAddress.Equals(ipAddress))) + { + var device = new PnpDev(false, name, ipAddress, macAddress); + devices.Add(device); + } + } + + return devices; + } + + public static Image GenerateGradientBitmap(int width, int height) + { + var bitmap = new Bitmap(512, height); + + for (var i = 0; i < 256; i++) + { + var pixel = Color.FromArgb(i, i, i); + + for (var h = 0; h < height; h++) + { + bitmap.SetPixel(i * 2, h, pixel); + bitmap.SetPixel(i * 2 + 1, h, pixel); + } + } + + return bitmap; + } + + public static string FirstCharUpperCase(string name) + { + return name.Substring(0, 1).ToUpper() + name.Substring(1); + } + + public static Dictionary EnumToDictionary(IEnumerable skipValues = null) where T : struct, Enum + { + return Enum.GetValues().Where(v => skipValues == null || !skipValues.Contains(v)).ToDictionary(k => k, e => e.GetDescription()); + } + + public static string GetDescriptionByEnumName(string value) where T : IConvertible + { + string description = null; + + if (Enum.TryParse(typeof(T), value, out var result)) + { + description = (result as IConvertible)?.GetDescription(); + } + + description = description ?? FirstCharUpperCase(value); + + return description; + } + + public static string GetDescription(this T e) where T : IConvertible + { + return GetDescription(e.GetType(), e); + } + + public static string GetDescription(Type enumType, IConvertible value) + { + if (enumType.BaseType == typeof(Enum)) + { + var val = value.ToInt32(CultureInfo.InvariantCulture); + + var enumName = enumType.GetEnumName(val); + if (enumName == null) + { + return string.Empty; + } + + var memInfo = enumType.GetMember(enumName); + var descriptionAttribute = memInfo[0] + .GetCustomAttributes(typeof(DescriptionAttribute), false) + .FirstOrDefault() as DescriptionAttribute; + + if (descriptionAttribute != null) + { + return descriptionAttribute.Description; + } + + return enumName; + } + + return null; // could also return string.Empty + } + + public static T GetEnumValueByDescription(string value) where T : struct + { + var enumName = GetEnumNameByDescription(typeof(T), value); + if (enumName != null) + { + value = enumName; + } + + return Enum.Parse(value); + } + + public static string RemoveFirstUnderscore(this string text) + { + if (string.IsNullOrEmpty(text)) + { + return text; + } + + if (text[0] == '_') + { + return text.Substring(1); + } + + return text; + } + + public static string GetEnumNameByDescription(Type enumType, string description) + { + if (enumType.BaseType == typeof(Enum)) + { + foreach (var enumValue in Enum.GetValues(enumType)) + { + if (description.Equals(GetDescription(enumType, enumValue as IConvertible), StringComparison.Ordinal)) + { + return Enum.GetName(enumType, enumValue).RemoveFirstUnderscore(); + } + } + } + + return null; // could also return string.Empty + } + + public static List GetDescriptions(int value = -1, int fromValue = 0) where T : IConvertible + { + return GetDescriptions(typeof(T), value, fromValue); + } + + public static List GetDescriptions(Type enumType, int value = -1, int fromValue = 0, bool replaceUnderscore = false) + { + var list = new List(); + foreach (var enumValue in Enum.GetValues(enumType)) + { + if (value > -1 || fromValue > 0) + { + var enumIntValue = Convert.ToInt32(enumValue); + + if (enumIntValue < fromValue || value >= 0 && (enumIntValue & value) == 0) + { + continue; + } + } + + list.Add(GetDescription(enumType, enumValue as IConvertible) ?? (replaceUnderscore ? enumValue.ToString().Replace("_", "") : enumValue.ToString())); + } + return list; + } + + public static bool OpenConsole() + { + if (ConsoleOpened) + { + return true; + } + + if (!WinApi.AttachConsole(-1)) + { + WinApi.AllocConsole(); + + SetConsoleWriter(); + + return true; + } + ConsoleOpened = true; + + SetConsoleWriter(); + + return false; + } + + private static void SetConsoleWriter() + { + var writer = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true }; + Console.SetOut(writer); + } + + public static bool CloseConsole() + { + if (!ConsoleOpened) + { + return false; + } + + SendKeys.SendWait("{ENTER}"); + var result = WinApi.FreeConsole(); + + return result; + } + + public static Process GetProcessByName(string name, bool skipCurrent = true) + { + var currentProcessId = Environment.ProcessId; + foreach (var process in Process.GetProcesses()) + { + if (process.ProcessName.Equals(name) && (!skipCurrent || process.Id != currentProcessId)) + { + return process; + } + } + + return null; + } + + public static string GetResourceFile(string resourceName) + { + resourceName = "ColorControl.Resources." + resourceName; + + var assembly = Assembly.GetEntryAssembly(); + + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + using (StreamReader reader = new StreamReader(stream)) + { + var result = reader.ReadToEnd(); + + return result; + } + } + + public static void ParseWords(List target, string text) + { + target.Clear(); + + if (string.IsNullOrEmpty(text)) + { + return; + } + var words = text.Split(','); + for (var i = 0; i < words.Length; i++) + { + var word = words[i]; + if (word.IndexOf("(") > -1) + { + words[i] = word.Trim(); + } + else + { + words[i] = word.Replace(" ", string.Empty); + } + } + target.AddRange(words); + } + + public static int ParseInt(string value, int def = 0) + { + if (int.TryParse(value, out var result)) + { + return result; + } + + return def; + } + + public static uint ParseUInt(string value, uint def = 0) + { + if (uint.TryParse(value, out var result)) + { + return result; + } + + return def; + } + + public static int ParseDynamicAsInt(dynamic value, int def = 0) + { + if (value.Type == JTokenType.Integer) + { + return (int)value.Value; + } + if (value.Type == JTokenType.String) + { + return ParseInt(value.Value, def); + } + return def; + } + + public static async Task GetRestJsonAsync(string url) + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("request"); + + var response = await httpClient.GetAsync(url); + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadAsStringAsync(); + + var data = JsonConvert.DeserializeObject(result); + + return data; + } + + return null; + } + + public static FileInfo SelectFile(string ext = "*.exe", string filter = "Application EXE Name|*.exe|Application Absolute Path|*.exe") + { + var openDialog = new System.Windows.Forms.OpenFileDialog(); + openDialog.DefaultExt = ext; + openDialog.Filter = filter; + + if (openDialog.ShowDialog() == DialogResult.OK) + { + return new FileInfo(openDialog.FileName); + //if (openDialog.FilterIndex == 2) + // applicationName = openDialog.FileName; + + //return applicationName; + } + + return null; + } + + public static bool NormEquals(this string str1, string str2) + { + return str1.Trim().Replace(" ", string.Empty).Equals(str2.Trim().Replace(" ", string.Empty), StringComparison.OrdinalIgnoreCase); + } + + public static async Task PingHost(string nameOrAddress) + { + try + { + using var pinger = new Ping(); + var reply = await pinger.SendPingAsync(nameOrAddress, 2000); + + return reply.Status == IPStatus.Success; + } + catch (PingException) + { + // Discard PingExceptions and return false; + return false; + } + } + + public static bool WriteText(string fileName, string data) + { + var doWrite = !File.Exists(fileName); + + if (!doWrite) + { + var writtenData = File.ReadAllText(fileName); + + doWrite = !writtenData.Equals(data); + } + + if (doWrite) + { + try + { + File.WriteAllText(fileName, data); + } + catch (Exception e) + { + Logger.Error(e.ToLogString()); + } + } + + return doWrite; + } + + public static string ReadText(string fileName, bool reverse = false) + { + var exists = File.Exists(fileName); + + if (!exists) + { + return null; + } + + try + { + var lines = new StringBuilder(); + + using var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + using var reader = new StreamReader(fs); + + string line; + + while ((line = reader.ReadLine()) != null) + { + if (reverse) + { + lines.Insert(0, line + "\r\n"); + continue; + } + + lines.AppendLine(line); + } + + return lines.ToString(); + } + catch (Exception e) + { + Logger.Error(e.ToLogString()); + } + + return null; + } + + public static List ReadLines(string fileName, bool reverse = false) + { + var exists = File.Exists(fileName); + + if (!exists) + { + return null; + } + + try + { + var lines = new List(); + + using var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + using var reader = new StreamReader(fs); + + string line; + + while ((line = reader.ReadLine()) != null) + { + if (reverse) + { + lines.Insert(0, line + "\r\n"); + continue; + } + + lines.Add(line); + } + + return lines; + } + catch (Exception e) + { + Logger.Error(e.ToLogString()); + } + + return null; + } + + public static bool WriteObject(string fileName, object value) + { + var data = JsonConvert.SerializeObject(value); + + return WriteText(fileName, data); + } + + public static T DeserializeJson(string fileName) + { + try + { + var json = File.ReadAllText(fileName); + var data = JsonConvert.DeserializeObject(json); + + return data; + } + catch (Exception e) + { + Logger.Error(e.ToLogString()); + + return default; + } + } + + public static string Base64Encode(string plainText) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public static string Base64Decode(string base64EncodedData) + { + var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); + return Encoding.UTF8.GetString(base64EncodedBytes); + } + + public static async Task DownloadFileAsync(string url, string filePath) + { + var httpClient = new HttpClient(); + using var stream = await httpClient.GetStreamAsync(url); + + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + using var fileStream = new FileStream(filePath, FileMode.CreateNew); + + await stream.CopyToAsync(fileStream); + } + + public static void UnZipFile(string zipFile, string filePath) + { + ZipFile.ExtractToDirectory(zipFile, filePath); + } + + public static void UpdateFiles(string clientPath, string updatePath) + { + var infos = new List(); + + GetFileSystemInfos(updatePath, infos); + + foreach (var info in infos) + { + var subPath = info.FullName.Replace(updatePath, ""); + + if (subPath[0] == '\\') + { + subPath = subPath.Substring(1); + } + + var targetPath = Path.Combine(clientPath, subPath); + + //Logger.Debug($"Path: {updatePath}, info: {info.FullName}, target: {targetPath}"); + + if (info is DirectoryInfo) + { + if (!Directory.Exists(targetPath)) + { + Directory.CreateDirectory(targetPath); + } + + continue; + } + + var fileInfo = info as FileInfo; + + var oldPath = targetPath + ".old"; + + if (File.Exists(oldPath)) + { + File.Delete(oldPath); + } + + if (File.Exists(targetPath)) + { + if (CompareFiles(fileInfo.FullName, targetPath)) + { + continue; + } + + File.Move(targetPath, targetPath + ".old"); + } + + fileInfo.CopyTo(targetPath); + } + } + + public static void GetFileSystemInfos(string path, List infos) + { + var directory = new DirectoryInfo(path); + + foreach (var dir in directory.GetDirectories()) + { + infos.Add(dir); + GetFileSystemInfos(dir.FullName, infos); + } + + foreach (var file in directory.GetFiles()) + { + infos.Add(file); + } + } + + public static string SHA256CheckSum(string filePath) + { + using (SHA256 SHA256 = SHA256.Create()) + { + using (FileStream fileStream = File.OpenRead(filePath)) + return Convert.ToBase64String(SHA256.ComputeHash(fileStream)); + } + } + + public static bool CompareFiles(string filePath1, string filePath2) + { + return SHA256CheckSum(filePath1) == SHA256CheckSum(filePath2); + } + + public static string ToKiloUnitString(this uint value, int div = 1000, string units = "MHz") => $"{value / div}{units}"; + public static string ToKiloUnitString(this int value, int div = 1000, string units = "MHz") => $"{value / div}{units}"; + public static string ToUnitString(this uint value, string units = "MHz") => $"{value}{units}"; + public static string ToSignedUnitString(this int value, string units = "MHz") => $"{(value >= 0 ? "+" + value : value)}{units}"; + public static string ToSignedUnitString(this uint value, string units = "MHz") => $"{(value >= 0 ? "+" + value : value)}{units}"; + public static string ToUnitString(string units = "°", params double?[] values) + { + var notNullValues = values.Where(v => v.HasValue).Select(v => $"{v}{units}"); + + return string.Join("/", notNullValues); + } + + public static System.Windows.Application EnsureApplication() + { + return System.Windows.Application.Current ?? new System.Windows.Application { ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown }; + } + } } diff --git a/Shared/Contracts/NVIDIA/NvSettingItemDto.cs b/Shared/Contracts/NVIDIA/NvSettingItemDto.cs index b198ca3..5e4c8db 100644 --- a/Shared/Contracts/NVIDIA/NvSettingItemDto.cs +++ b/Shared/Contracts/NVIDIA/NvSettingItemDto.cs @@ -4,64 +4,65 @@ namespace ColorControl.Shared.Contracts.NVIDIA; public enum NvSettingType { - DwordType, - BinaryType + DwordType, + BinaryType } public class NvSettingItemDto { - public uint SettingId { get; set; } + public uint SettingId { get; set; } - public string SettingText { get; set; } + public string SettingText { get; set; } - public string ValueText { get; set; } + public string ValueText { get; set; } - public string ValueRaw { get; set; } + public string ValueRaw { get; set; } - [JsonIgnore] - public bool ValueChanged { get; set; } + [JsonIgnore] + public bool ValueChanged { get; set; } - private uint _value; + private uint _value; - public uint Value - { - get - { - return _value; - } + public uint Value + { + get + { + return _value; + } - set - { - _value = value; - ValueChanged = true; - } - } + set + { + _value = value; + ValueChanged = true; + } + } - public string GroupName { get; set; } + public string GroupName { get; set; } - public int State { get; set; } + public int State { get; set; } - public bool IsStringValue { get; set; } + public bool IsStringValue { get; set; } - public bool IsApiExposed { get; set; } + public bool IsApiExposed { get; set; } - public bool IsSettingHidden { get; set; } + public bool IsSettingHidden { get; set; } - public NvSettingType SettingType { get; set; } + public NvSettingType SettingType { get; set; } - public uint DefaultValue { get; set; } + public uint DefaultValue { get; set; } - public List Values { get; set; } + public List Values { get; set; } + public string FriendlyName { get; set; } - public override string ToString() - { - return string.Format("{0}; 0x{1:X8}; {2}; {3}; {4};", State, SettingId, SettingText, ValueText, ValueRaw); - } + public override string ToString() + { + return string.Format("{0}; 0x{1:X8}; {2}; {3}; {4};", State, SettingId, SettingText, ValueText, ValueRaw); + } } public class NvSettingItemValue { - public uint Value { get; set; } - public string ValueName { get; set; } + public uint Value { get; set; } + public string ValueName { get; set; } } \ No newline at end of file diff --git a/Shared/EventDispatcher/DisplayChangeEventDispatcher.cs b/Shared/EventDispatcher/DisplayChangeEventDispatcher.cs index 2ea4a1c..3ad2902 100644 --- a/Shared/EventDispatcher/DisplayChangeEventDispatcher.cs +++ b/Shared/EventDispatcher/DisplayChangeEventDispatcher.cs @@ -1,17 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static NStandard.Measures.Length; - -namespace ColorControl.Shared.EventDispatcher; +namespace ColorControl.Shared.EventDispatcher; public class DisplayChangedEventArgs : EventArgs { public uint Width { get; set; } public uint Height { get; set; } - public uint BitsPerPixel{ get; set; } + public uint BitsPerPixel { get; set; } } public class DisplayChangeEventDispatcher : EventDispatcher diff --git a/Shared/EventDispatcher/KeyboardShortcutDispatcher.cs b/Shared/EventDispatcher/KeyboardShortcutDispatcher.cs index e8f87e2..607db7e 100644 --- a/Shared/EventDispatcher/KeyboardShortcutDispatcher.cs +++ b/Shared/EventDispatcher/KeyboardShortcutDispatcher.cs @@ -2,6 +2,7 @@ using ColorControl.Shared.Forms; using Linearstar.Windows.RawInput; using Linearstar.Windows.RawInput.Native; +using NStandard; namespace ColorControl.Shared.EventDispatcher; @@ -423,11 +424,11 @@ public class KeyboardShortcutDispatcher : EventDispatcher !s.IsNullOrWhiteSpace()).Count() > 1 || ShortcutIsAllowedWithoutModifier(shortcut); - if (!valid) + if (!valid && showMessage) { MessageForms.WarningOk("Invalid shortcut. The shortcut should have modifiers and a normal key or be F13-F24."); } diff --git a/Shared/Forms/DarkModeUtils.cs b/Shared/Forms/DarkModeUtils.cs index 0af11f2..2759ab5 100644 --- a/Shared/Forms/DarkModeUtils.cs +++ b/Shared/Forms/DarkModeUtils.cs @@ -124,12 +124,6 @@ namespace ColorControl.Shared.Forms try { - //var settings = new UISettings(); - - //var foregroundColorValue = settings.GetColorValue(UIColorType.Foreground); - //var backroundColorValue = settings.GetColorValue(UIColorType.Background); - //var isDarkMode = FormUtils.IsColorLight(foregroundColorValue); - SetControlTheme(form, FormUtils.CurrentBackColor, FormUtils.CurrentForeColor); var value = toDark ? 1 : 0; @@ -240,10 +234,5 @@ namespace ColorControl.Shared.Forms SetMenuForeColor(item, color); } } - - public static bool IsColorLight(Windows.UI.Color color) - { - return ((5 * color.G) + (2 * color.R) + color.B) > (8 * 128); - } } } diff --git a/Shared/Native/PnpDev.cs b/Shared/Native/PnpDev.cs index 5b031c8..3639906 100644 --- a/Shared/Native/PnpDev.cs +++ b/Shared/Native/PnpDev.cs @@ -1,12 +1,8 @@ -using Windows.Devices.Enumeration; -using Windows.Devices.Enumeration.Pnp; - -namespace ColorControl.Shared.Native +namespace ColorControl.Shared.Native { public class PnpDev { - public DeviceInformation DevInfo { get; private set; } - public PnpObject PnpObject { get; private set; } + public bool IsCustom { get; private set; } public string Name { get; private set; } public string IpAddress { get; private set; } @@ -15,24 +11,20 @@ namespace ColorControl.Shared.Native public PnpDev(string name, string ipAddress, string macAddress) { + IsCustom = true; Name = name; IpAddress = ipAddress; MacAddress = macAddress; } - public PnpDev(DeviceInformation devInfo, PnpObject pnpObject, string name, string ipAddress, string macAddress) : this(name, ipAddress, macAddress) + + public PnpDev(bool isCustom, string name, string ipAddress, string macAddress) : this(name, ipAddress, macAddress) { - DevInfo = devInfo; - PnpObject = pnpObject; + IsCustom = isCustom; } public override string ToString() { - return (IsCustom() ? "Custom: " : "Auto detect: ") + $"{Name}" + (!string.IsNullOrEmpty(IpAddress) ? $"({IpAddress})" : string.Empty); - } - - public bool IsCustom() - { - return DevInfo == null; + return (IsCustom ? "Custom: " : "Auto detect: ") + $"{Name}" + (!string.IsNullOrEmpty(IpAddress) ? $"({IpAddress})" : string.Empty); } } } diff --git a/Shared/Services/WinApiAdminService.cs b/Shared/Services/WinApiAdminService.cs index e15891a..029b273 100644 --- a/Shared/Services/WinApiAdminService.cs +++ b/Shared/Services/WinApiAdminService.cs @@ -317,7 +317,7 @@ namespace ColorControl.Shared.Services try { - using (TaskService ts = new TaskService()) + using (var ts = new TaskService()) { if (enabled) { @@ -329,6 +329,9 @@ namespace ColorControl.Shared.Services td.Triggers.Add(new LogonTrigger { UserId = WindowsIdentity.GetCurrent().Name }); td.Actions.Add(new ExecAction(file, StartUpParams.RunningFromScheduledTaskParam, directory)); + td.Settings.DisallowStartIfOnBatteries = false; + td.Settings.StopIfGoingOnBatteries = false; + td.Settings.ExecutionTimeLimit = TimeSpan.Zero; ts.RootFolder.RegisterTaskDefinition(taskName, td); } diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index c7bd95f..c768d6d 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -1,11 +1,12 @@  - net8.0-windows10.0.20348.0 + net9.0-windows enable disable true true + None $(MSBuildThisFileDirectory)=.\ @@ -25,14 +26,14 @@ - + - - - - - - + + + + + + diff --git a/novideo_srgb/novideo_srgb.csproj b/novideo_srgb/novideo_srgb.csproj index db60ae6..5840556 100644 --- a/novideo_srgb/novideo_srgb.csproj +++ b/novideo_srgb/novideo_srgb.csproj @@ -1,14 +1,14 @@  - net8.0-windows10.0.20348.0 + net9.0-windows enable enable Properties true true true - 10.0.20348.0 + None @@ -16,8 +16,8 @@ - - + +