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