From a6dc2b62301742f068f86794a7ed4019e375e520 Mon Sep 17 00:00:00 2001 From: ponsy Date: Tue, 25 Aug 2026 02:41:02 +0200 Subject: [PATCH] Add http action type and Sonar mute toggle (chatCapture, media channels) --- host-app/MacroPad.Host/ActionFactory.cs | 6 +- host-app/MacroPad.Host/Config.cs | 3 +- host-app/MacroPad.Host/HttpAction.cs | 38 +++++++ host-app/MacroPad.Host/KeystrokeAction.cs | 2 + host-app/MacroPad.Host/Program.cs | 99 +++++++++++++------ .../MacroPad.Host/SonarAddressResolver.cs | 43 ++++++++ .../MacroPad.Host/SonarToggleMuteAction.cs | 60 +++++++++++ host-app/MacroPad.Host/config.json | 9 +- 8 files changed, 227 insertions(+), 33 deletions(-) create mode 100644 host-app/MacroPad.Host/HttpAction.cs create mode 100644 host-app/MacroPad.Host/SonarAddressResolver.cs create mode 100644 host-app/MacroPad.Host/SonarToggleMuteAction.cs diff --git a/host-app/MacroPad.Host/ActionFactory.cs b/host-app/MacroPad.Host/ActionFactory.cs index 72c045e..12aa086 100644 --- a/host-app/MacroPad.Host/ActionFactory.cs +++ b/host-app/MacroPad.Host/ActionFactory.cs @@ -1,12 +1,16 @@ public static class ActionFactory { - public static IAction? Create(BindingConfig config) + public static IAction? Create(BindingConfig config, string? sonarAddress) { return config.Type switch { "launch" => new LaunchAction(config.Target), "url" => new UrlAction(config.Target), "keystroke" => new KeystrokeAction(config.Target), + "http" => new HttpAction(config.Method, config.Target, config.Body), + "sonar-toggle-mute" => sonarAddress is not null + ? new SonarToggleMuteAction(sonarAddress, config.Target) + : null, _ => null }; } diff --git a/host-app/MacroPad.Host/Config.cs b/host-app/MacroPad.Host/Config.cs index e09e9aa..5c4b0c5 100644 --- a/host-app/MacroPad.Host/Config.cs +++ b/host-app/MacroPad.Host/Config.cs @@ -2,8 +2,9 @@ public class BindingConfig { public string Type { get; set; } = ""; public string Target { get; set; } = ""; + public string Method { get; set; } = "GET"; + public string? Body { get; set; } } - public class Config { public Dictionary Bindings { get; set; } = new(); diff --git a/host-app/MacroPad.Host/HttpAction.cs b/host-app/MacroPad.Host/HttpAction.cs new file mode 100644 index 0000000..1083a60 --- /dev/null +++ b/host-app/MacroPad.Host/HttpAction.cs @@ -0,0 +1,38 @@ +using System.Net.Http; +using System.Text; + +public class HttpAction : IAction +{ + private static readonly HttpClient Client = new(); + + private readonly HttpMethod _method; + private readonly string _url; + private readonly string? _body; + + public HttpAction(string method, string url, string? body) + { + _method = new HttpMethod(method.ToUpperInvariant()); + _url = url; + _body = body; + } + + public void Execute() + { + var request = new HttpRequestMessage(_method, _url); + if (_body is not null) + request.Content = new StringContent(_body, Encoding.UTF8, "application/json"); + + Client.SendAsync(request).ContinueWith(async t => + { + if (t.IsFaulted) + { + Console.WriteLine($"[HttpAction] Échec requête {_method} {_url} : {t.Exception?.GetBaseException().Message}"); + return; + } + + var response = t.Result; + var body = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"[HttpAction] {_method} {_url} → HTTP {(int)response.StatusCode} : {body}"); + }, TaskScheduler.Default); + } +} \ No newline at end of file diff --git a/host-app/MacroPad.Host/KeystrokeAction.cs b/host-app/MacroPad.Host/KeystrokeAction.cs index aa9b787..edbcc84 100644 --- a/host-app/MacroPad.Host/KeystrokeAction.cs +++ b/host-app/MacroPad.Host/KeystrokeAction.cs @@ -31,6 +31,8 @@ public class KeystrokeAction : IAction, IDisposable "enter" or "return" => KeyCode.VcEnter, "space" => KeyCode.VcSpace, "delete" or "del" => KeyCode.VcDelete, + _ when System.Text.RegularExpressions.Regex.IsMatch(name, @"^f([1-9]|1[0-9]|2[0-4])$") + => Enum.Parse($"Vc{name.ToUpperInvariant()}"), _ when name.Length == 1 => Enum.Parse($"Vc{char.ToUpperInvariant(name[0])}"), _ => throw new ArgumentException($"Touche inconnue : {name}") }; diff --git a/host-app/MacroPad.Host/Program.cs b/host-app/MacroPad.Host/Program.cs index 140aa97..888876a 100644 --- a/host-app/MacroPad.Host/Program.cs +++ b/host-app/MacroPad.Host/Program.cs @@ -6,53 +6,92 @@ var configJson = File.ReadAllText(configPath); var config = JsonSerializer.Deserialize(configJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new Config(); +var sonarAddress = await SonarAddressResolver.GetSonarAddressAsync(); +Console.WriteLine($"Adresse Sonar résolue : {sonarAddress ?? "AUCUNE"}"); + var actions = new Dictionary(); foreach (var (key, binding) in config.Bindings) { - var action = ActionFactory.Create(binding); + if (binding.Target.Contains("{sonar}") && sonarAddress is not null) + binding.Target = binding.Target.Replace("{sonar}", sonarAddress); + + var action = ActionFactory.Create(binding, sonarAddress); if (action is not null && int.TryParse(key, out var bit)) actions[bit] = action; } -var device = DeviceList.Local.GetHidDevices(vendorID: 0x2341, productID: 0x8036).FirstOrDefault(); -if (device is null) -{ - Console.WriteLine("Pad non trouvé. Vérifie qu'il est branché."); - return; -} +Console.WriteLine($"{actions.Count} binding(s) chargé(s) :"); +foreach (var (bit, _) in actions) + Console.WriteLine($" bit {bit} -> {config.Bindings.First(b => int.Parse(b.Key) == bit).Value.Type}"); +Console.WriteLine(); -using var stream = device.Open(); -stream.ReadTimeout = Timeout.Infinite; -var buffer = new byte[device.GetMaxInputReportLength()]; -ushort lastMask = 0; - -Console.WriteLine("En écoute...\n"); +const ushort VendorId = 0x2341; +const ushort ProductId = 0x8036; while (true) { - int count; - try { count = stream.Read(buffer, 0, buffer.Length); } - catch (TimeoutException) { continue; } + var device = DeviceList.Local.GetHidDevices(vendorID: VendorId, productID: ProductId).FirstOrDefault(); - int offset = count > 2 ? 1 : 0; - ushort mask = (ushort)(buffer[offset] | (buffer[offset + 1] << 8)); - - if (mask != lastMask) + if (device is null) { - ushort changed = (ushort)(mask ^ lastMask); - for (int bit = 0; bit < 12; bit++) - { - bool bitChanged = (changed & (1 << bit)) != 0; - bool nowPressed = (mask & (1 << bit)) != 0; + Console.WriteLine("Pad non trouvé, nouvelle tentative dans 2s..."); + Thread.Sleep(2000); + continue; + } - // On déclenche seulement au moment où le bouton est pressé, pas relâché - if (bitChanged && nowPressed && actions.TryGetValue(bit, out var action)) + Console.WriteLine($"Pad connecté : {device.DevicePath}"); + + try + { + ListenToDevice(device, actions); + } + catch (Exception ex) when (ex is IOException or TimeoutException or ObjectDisposedException) + { + Console.WriteLine("Pad déconnecté. En attente de reconnexion..."); + } +} + +void ListenToDevice(HidDevice device, Dictionary actions) +{ + using var stream = device.Open(); + stream.ReadTimeout = Timeout.Infinite; + + var buffer = new byte[device.GetMaxInputReportLength()]; + ushort lastMask = 0; + + while (true) + { + int count = stream.Read(buffer, 0, buffer.Length); + if (count == 0) throw new IOException("Rapport vide, device probablement déconnecté"); + + int offset = count > 2 ? 1 : 0; + ushort mask = (ushort)(buffer[offset] | (buffer[offset + 1] << 8)); + + Console.WriteLine($"Rapport reçu, mask={Convert.ToString(mask, 2).PadLeft(12, '0')}"); + + if (mask != lastMask) + { + ushort changed = (ushort)(mask ^ lastMask); + for (int bit = 0; bit < 12; bit++) { - Console.WriteLine($"Bouton {bit} → exécution de l'action"); - action.Execute(); + bool bitChanged = (changed & (1 << bit)) != 0; + bool nowPressed = (mask & (1 << bit)) != 0; + + if (bitChanged && nowPressed && actions.TryGetValue(bit, out var action)) + { + Console.WriteLine($"Bouton {bit} → exécution de l'action"); + try + { + action.Execute(); + } + catch (Exception ex) + { + Console.WriteLine($"Erreur lors de l'exécution : {ex.Message}"); + } + } } + lastMask = mask; } - lastMask = mask; } } \ No newline at end of file diff --git a/host-app/MacroPad.Host/SonarAddressResolver.cs b/host-app/MacroPad.Host/SonarAddressResolver.cs new file mode 100644 index 0000000..85b8c39 --- /dev/null +++ b/host-app/MacroPad.Host/SonarAddressResolver.cs @@ -0,0 +1,43 @@ +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Text.Json; + +public static class SonarAddressResolver +{ + private static readonly HttpClient Client = CreateInsecureClient(); + + private static HttpClient CreateInsecureClient() + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true + }; + return new HttpClient(handler); + } + + public static async Task GetSonarAddressAsync() + { + var corePropsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "SteelSeries", "SteelSeries Engine 3", "coreProps.json"); + + if (!File.Exists(corePropsPath)) + return null; + + using var coreDoc = JsonDocument.Parse(File.ReadAllText(corePropsPath)); + if (!coreDoc.RootElement.TryGetProperty("ggEncryptedAddress", out var ggAddr)) + return null; + + var subAppsJson = await Client.GetStringAsync($"https://{ggAddr.GetString()}/subApps"); + using var subAppsDoc = JsonDocument.Parse(subAppsJson); + + var webServerAddress = subAppsDoc.RootElement + .GetProperty("subApps") + .GetProperty("sonar") + .GetProperty("metadata") + .GetProperty("webServerAddress") + .GetString(); + + return webServerAddress?.Replace("http://", "").Replace("https://", ""); + } +} \ No newline at end of file diff --git a/host-app/MacroPad.Host/SonarToggleMuteAction.cs b/host-app/MacroPad.Host/SonarToggleMuteAction.cs new file mode 100644 index 0000000..a98a037 --- /dev/null +++ b/host-app/MacroPad.Host/SonarToggleMuteAction.cs @@ -0,0 +1,60 @@ +using System.Net.Http; +using System.Text.Json; + +public class SonarToggleMuteAction : IAction +{ + private static readonly HttpClient Client = CreateClient(); + + private readonly string _address; + private readonly string _channel; + private bool? _lastKnownMuted; + + public SonarToggleMuteAction(string address, string channel) + { + _address = address; + _channel = channel; + } + + public void Execute() + { + _ = ToggleAsync(); + } + + private async Task ToggleAsync() + { + try + { + // Première pression : on part du principe qu'on est démuté, donc on mute. + // Ensuite, on se base sur l'état réel renvoyé par Sonar à chaque appel. + var desiredMuted = !(_lastKnownMuted ?? false); + + var url = $"http://{_address}/volumeSettings/classic/{_channel}/Mute/{(desiredMuted ? "true" : "false")}"; + var response = await Client.PutAsync(url, new StringContent("")); + var body = await response.Content.ReadAsStringAsync(); + + using var doc = JsonDocument.Parse(body); + var actualMuted = doc.RootElement + .GetProperty("devices") + .GetProperty(_channel) + .GetProperty("classic") + .GetProperty("muted") + .GetBoolean(); + + _lastKnownMuted = actualMuted; + Console.WriteLine($"[SonarToggleMute] {_channel} -> muted={actualMuted}"); + } + catch (Exception ex) + { + Console.WriteLine($"[SonarToggleMute] Erreur : {ex.Message}"); + } + } + + private static HttpClient CreateClient() + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true + }; + return new HttpClient(handler); + } +} \ No newline at end of file diff --git a/host-app/MacroPad.Host/config.json b/host-app/MacroPad.Host/config.json index 71fc7ef..4d723cb 100644 --- a/host-app/MacroPad.Host/config.json +++ b/host-app/MacroPad.Host/config.json @@ -2,6 +2,13 @@ "bindings": { "0": { "type": "launch", "target": "notepad.exe" }, "1": { "type": "url", "target": "https://github.com" }, - "2": { "type": "keystroke", "target": "ctrl+shift+esc" } + "2": { "type": "keystroke", "target": "ctrl+shift+esc" }, + "3": { "type": "keystroke", "target": "f13" }, + "4": { + "type": "http", + "method": "PUT", + "target": "http://{sonar}/volumeSettings/classic/chatCapture/Mute/true" + }, + "5": { "type": "sonar-toggle-mute", "target": "media" } } } \ No newline at end of file