From c974f0bc91b018171eea25f74b436d114cccde54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:44:47 +0000 Subject: [PATCH 1/4] Initial plan From a01c914543931e6539e1ee8f3b0e05f61650088a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:05:04 +0000 Subject: [PATCH 2/4] fix: fetch badge type from GitHub subscriptions.json instead of Arduino serial data The Arduino serial data always sends 0 for the badge field since it has no way to determine subscription tiers. Badge type is now looked up from a subscriptions.json file hosted on GitHub, keyed by the device's USB serial number. This decouples badge display from the serial protocol and aligns with the direction proposed in issue #4. Closes #15 Co-authored-by: PalmarHealer <93807726+PalmarHealer@users.noreply.github.com> --- ControlPad/Arduino/ArduinoController.cs | 20 +++------ ControlPad/Arduino/ArduinoPortFinder.cs | 21 ++++++--- ControlPad/SubscriptionService.cs | 59 +++++++++++++++++++++++++ subscriptions.json | 3 ++ 4 files changed, 84 insertions(+), 19 deletions(-) create mode 100644 ControlPad/SubscriptionService.cs create mode 100644 subscriptions.json diff --git a/ControlPad/Arduino/ArduinoController.cs b/ControlPad/Arduino/ArduinoController.cs index 853eeee..1d5ff9c 100644 --- a/ControlPad/Arduino/ArduinoController.cs +++ b/ControlPad/Arduino/ArduinoController.cs @@ -61,7 +61,7 @@ public static void Initialize(MainWindow mainWindow, EventHandler eventHandler) private static async Task TryOpenAsync() { - string? port = await Task.Run(() => ArduinoPortFinder.FindFirstArduinoPort()); + var (port, deviceId) = await Task.Run(() => ArduinoPortFinder.FindFirstArduinoPort()); if (port == null) { await _mainWindow.Dispatcher.InvokeAsync(() => @@ -99,11 +99,16 @@ await _mainWindow.Dispatcher.InvokeAsync(() => _readCts = new CancellationTokenSource(); _ = Task.Run(() => ReadLoopAsync(sp, _readCts.Token)); + string? serial = ArduinoPortFinder.ExtractSerial(deviceId); + BadgeType badge = await SubscriptionService.GetBadgeTypeAsync(serial); + _lastBadgeType = badge; + await _mainWindow.Dispatcher.InvokeAsync(() => { _mainWindow.BoardDisconnectedInfoBar.IsOpen = false; if (_mainWindow.NVI_Home.IsActive) _mainWindow.MainContentFrame.Navigate(_mainWindow._homeUserControl); _mainWindow.NVI_EditMode.Visibility = Visibility.Visible; + _mainWindow.UpdateBadgeType(badge); IsConnected = true; }); } @@ -187,19 +192,6 @@ private static void ProcessLine(string line) } } - if (int.TryParse(inputs[1], out int badgeValue)) - { - var newBadgeType = (BadgeType)badgeValue; - if (_lastBadgeType != newBadgeType) - { - _mainWindow.Dispatcher.BeginInvoke(() => - { - _mainWindow.UpdateBadgeType(newBadgeType); - }); - _lastBadgeType = newBadgeType; - } - } - UpdateValues(inputs[2..]); // UI-Updates ggf. drosseln diff --git a/ControlPad/Arduino/ArduinoPortFinder.cs b/ControlPad/Arduino/ArduinoPortFinder.cs index d1868f9..5a0ee54 100644 --- a/ControlPad/Arduino/ArduinoPortFinder.cs +++ b/ControlPad/Arduino/ArduinoPortFinder.cs @@ -12,8 +12,8 @@ public static class ArduinoPortFinder "VID_0403" // FTDI-basierte Klone (ältere Duemilanove usw.) }; - /// Gibt z. B. "COM7" zurück oder null, wenn kein Arduino gefunden wurde. - public static string FindFirstArduinoPort() + /// Returns the COM port and PNPDeviceID, or (null, null) if no Arduino was found. + public static (string? port, string? deviceId) FindFirstArduinoPort() { foreach (string port in SerialPort.GetPortNames()) // 1. alle Ports holen { @@ -28,14 +28,25 @@ public static string FindFirstArduinoPort() // 2a) Originale melden sich mit "Arduino XY (COMn)" if (name.IndexOf("Arduino", StringComparison.OrdinalIgnoreCase) >= 0) - return port; + return (port, id); // 2b) Klone: auf Vendor-ID prüfen if (Array.Exists(KnownVids, vid => id.IndexOf(vid, StringComparison.OrdinalIgnoreCase) >= 0)) - return port; + return (port, id); } } - return null; // nichts gefunden + return (null, null); // nichts gefunden + } + + /// Extracts the serial number segment from a PNPDeviceID string (e.g. USB\VID_2341&PID_0043\SERIAL). + public static string? ExtractSerial(string? pnpDeviceId) + { + if (string.IsNullOrEmpty(pnpDeviceId)) + return null; + + // PNPDeviceID format: USB\VID_XXXX&PID_XXXX\SERIAL + var parts = pnpDeviceId.Split('\\'); + return parts.Length >= 3 ? parts[2] : null; } } diff --git a/ControlPad/SubscriptionService.cs b/ControlPad/SubscriptionService.cs new file mode 100644 index 0000000..530d021 --- /dev/null +++ b/ControlPad/SubscriptionService.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +namespace ControlPad +{ + public static class SubscriptionService + { + private const string SubscriptionsUrl = + "https://raw.githubusercontent.com/ControlPad/App/main/subscriptions.json"; + + private static readonly HttpClient _httpClient = new(); + + /// + /// Fetches the subscriptions file from GitHub and returns the badge type + /// for the given device serial number. + /// + public static async Task GetBadgeTypeAsync(string? serial) + { + if (string.IsNullOrEmpty(serial)) + return BadgeType.None; + + try + { + string json = await _httpClient.GetStringAsync(SubscriptionsUrl); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var data = JsonSerializer.Deserialize(json, options); + if (data?.Subscriptions == null) + return BadgeType.None; + + foreach (var entry in data.Subscriptions) + { + if (string.Equals(entry.SerialNumber, serial, StringComparison.OrdinalIgnoreCase)) + return (BadgeType)entry.BadgeType; + } + } + catch (Exception ex) + { + Debug.WriteLine($"Subscription fetch failed: {ex.Message}"); + } + + return BadgeType.None; + } + + private class SubscriptionsData + { + public List? Subscriptions { get; set; } + } + + private class SubscriptionEntry + { + public string SerialNumber { get; set; } = ""; + public int BadgeType { get; set; } + } + } +} diff --git a/subscriptions.json b/subscriptions.json new file mode 100644 index 0000000..59ecbf4 --- /dev/null +++ b/subscriptions.json @@ -0,0 +1,3 @@ +{ + "subscriptions": [] +} From 1c5f3aa47e6091ecd60b4717fe89791917ce06f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:06:11 +0000 Subject: [PATCH 3/4] address code review: add HttpClient timeout, enum validation, and per-serial cache Co-authored-by: PalmarHealer <93807726+PalmarHealer@users.noreply.github.com> --- ControlPad/SubscriptionService.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ControlPad/SubscriptionService.cs b/ControlPad/SubscriptionService.cs index 530d021..f887d7d 100644 --- a/ControlPad/SubscriptionService.cs +++ b/ControlPad/SubscriptionService.cs @@ -12,7 +12,8 @@ public static class SubscriptionService private const string SubscriptionsUrl = "https://raw.githubusercontent.com/ControlPad/App/main/subscriptions.json"; - private static readonly HttpClient _httpClient = new(); + private static readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(10) }; + private static readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); /// /// Fetches the subscriptions file from GitHub and returns the badge type @@ -23,6 +24,9 @@ public static async Task GetBadgeTypeAsync(string? serial) if (string.IsNullOrEmpty(serial)) return BadgeType.None; + if (_cache.TryGetValue(serial, out var cached)) + return cached; + try { string json = await _httpClient.GetStringAsync(SubscriptionsUrl); @@ -33,8 +37,14 @@ public static async Task GetBadgeTypeAsync(string? serial) foreach (var entry in data.Subscriptions) { - if (string.Equals(entry.SerialNumber, serial, StringComparison.OrdinalIgnoreCase)) - return (BadgeType)entry.BadgeType; + if (Enum.IsDefined(typeof(BadgeType), entry.BadgeType)) + { + var badge = (BadgeType)entry.BadgeType; + _cache[entry.SerialNumber] = badge; + + if (string.Equals(entry.SerialNumber, serial, StringComparison.OrdinalIgnoreCase)) + return badge; + } } } catch (Exception ex) From 9491b5f1365e31ab95f27104fdf091d1c66fc955 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:01:28 +0000 Subject: [PATCH 4/4] fix: restore serial data badge parsing alongside subscription service The previous fix removed badge parsing from serial data (inputs[1]) and replaced it with SubscriptionService. But the service returns None because subscriptions.json is empty, so badges never displayed. This restores the original PR #10 serial parsing so badges work from either source. Co-authored-by: PalmarHealer <93807726+PalmarHealer@users.noreply.github.com> --- ControlPad/Arduino/ArduinoController.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ControlPad/Arduino/ArduinoController.cs b/ControlPad/Arduino/ArduinoController.cs index 1d5ff9c..26f76a3 100644 --- a/ControlPad/Arduino/ArduinoController.cs +++ b/ControlPad/Arduino/ArduinoController.cs @@ -192,6 +192,19 @@ private static void ProcessLine(string line) } } + if (int.TryParse(inputs[1], out int badgeValue)) + { + var newBadgeType = (BadgeType)badgeValue; + if (_lastBadgeType != newBadgeType) + { + _mainWindow.Dispatcher.BeginInvoke(() => + { + _mainWindow.UpdateBadgeType(newBadgeType); + }); + _lastBadgeType = newBadgeType; + } + } + UpdateValues(inputs[2..]); // UI-Updates ggf. drosseln