From 7deed9b240b7d6514592016eff8bf56e3b4fa578 Mon Sep 17 00:00:00 2001 From: Jrdiver Date: Fri, 27 Dec 2024 02:09:02 -0600 Subject: [PATCH 1/6] V1 of Single LED control --- src/Kevsoft.WLED/Kevsoft.WLED.csproj | 4 ++-- src/Kevsoft.WLED/SegmentRequest.cs | 3 +++ src/Kevsoft.WLED/SingleLed.cs | 10 ++++++++++ src/Kevsoft.WLED/WLedClient.cs | 29 +++++++++++++++++++++++++--- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/Kevsoft.WLED/SingleLed.cs diff --git a/src/Kevsoft.WLED/Kevsoft.WLED.csproj b/src/Kevsoft.WLED/Kevsoft.WLED.csproj index c0b2c4b..3898f26 100644 --- a/src/Kevsoft.WLED/Kevsoft.WLED.csproj +++ b/src/Kevsoft.WLED/Kevsoft.WLED.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/src/Kevsoft.WLED/SegmentRequest.cs b/src/Kevsoft.WLED/SegmentRequest.cs index 4dcd34f..d1486b7 100644 --- a/src/Kevsoft.WLED/SegmentRequest.cs +++ b/src/Kevsoft.WLED/SegmentRequest.cs @@ -87,6 +87,9 @@ public sealed class SegmentRequest [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? Mirror { get; set; } + [JsonPropertyName("i")] + public object[] IndividualLedControl { get; set; } = []; + public static SegmentRequest From(SegmentResponse segmentResponse) { diff --git a/src/Kevsoft.WLED/SingleLed.cs b/src/Kevsoft.WLED/SingleLed.cs new file mode 100644 index 0000000..8de04ab --- /dev/null +++ b/src/Kevsoft.WLED/SingleLed.cs @@ -0,0 +1,10 @@ +namespace Kevsoft.WLED; + +public sealed class SingleLed +{ + /// The position of the LED in the segment + public int LedPosition { get; set; } + + /// The color of the LED as HEX (e.g. FF0000 for red) + public string Color { get; set; } = string.Empty; +} diff --git a/src/Kevsoft.WLED/WLedClient.cs b/src/Kevsoft.WLED/WLedClient.cs index ccddbba..1189d85 100644 --- a/src/Kevsoft.WLED/WLedClient.cs +++ b/src/Kevsoft.WLED/WLedClient.cs @@ -61,7 +61,7 @@ public async Task GetPalettes() var message = await _client.GetAsync("json/pal"); message.EnsureSuccessStatusCode(); - + return (await message.Content.ReadFromJsonAsync())!; } @@ -73,7 +73,7 @@ public async Task Post(WLedRootRequest request) var result = await _client.PostAsync("/json", content); result.EnsureSuccessStatusCode(); } - + public async Task Post(StateRequest request) { var stateString = JsonSerializer.Serialize(request); @@ -82,4 +82,27 @@ public async Task Post(StateRequest request) var result = await _client.PostAsync("/json/state", content); result.EnsureSuccessStatusCode(); } -} \ No newline at end of file + + public async Task Post(List ledList) + { + // Eliminate duplicate positions + ledList = ledList.GroupBy(x => x.LedPosition).Select(x => x.Last()).ToList(); + + List list = []; + int counter = 0; + + foreach (SingleLed led in ledList) + { + if (counter > 255) + { + await Post(new StateRequest { Segments = [new() { Id = 0, IndividualLedControl = [.. list] }] }); + list = []; + counter = 0; + } + list.Add(led.LedPosition); + list.Add(led.Color); + counter++; + } + await Post(new StateRequest { Segments = [new() { Id = 0, IndividualLedControl = [.. list] }] }); + } +} From 7220b68ae0968bb6d094993d8eb60cd3db46e84b Mon Sep 17 00:00:00 2001 From: Jrdiver Date: Fri, 27 Dec 2024 16:44:43 -0600 Subject: [PATCH 2/6] Indiviudal LED Control V2 - Grouping added to limit posts --- src/Kevsoft.WLED/WLedClient.cs | 61 ++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/Kevsoft.WLED/WLedClient.cs b/src/Kevsoft.WLED/WLedClient.cs index 1189d85..b786df8 100644 --- a/src/Kevsoft.WLED/WLedClient.cs +++ b/src/Kevsoft.WLED/WLedClient.cs @@ -91,18 +91,65 @@ public async Task Post(List ledList) List list = []; int counter = 0; - foreach (SingleLed led in ledList) + //Attempt to group colors together to reduce the number of packets sent as there is a 256 color at a time limit + foreach (IGrouping? leds in ledList.GroupBy(x => x.Color)) { - if (counter > 255) + if (counter >=255) { - await Post(new StateRequest { Segments = [new() { Id = 0, IndividualLedControl = [.. list] }] }); + await Post(new StateRequest { On = true, Segments = [new() { Id = 0, IndividualLedControl = [.. list] }] }); list = []; counter = 0; } - list.Add(led.LedPosition); - list.Add(led.Color); - counter++; + // If there is only one LED in the group, add it to the list + if (leds.Count() == 1) + { + list.Add(leds.First().LedPosition); + list.Add(leds.First().Color); + counter++; + continue; + } + + // If there are multiple LEDs in the group, find the sequential LED's and group them up + // to make the next step easier + List> grouped = leds.Select(x => x.LedPosition).OrderBy(x => x) + .Aggregate(new List> { new() }, + (acc, curr) => + { + if (!acc.Last().Any() || curr - acc.Last().Last() == 1) + acc.Last().Add(curr); + else + acc.Add([curr]); + return acc; + }); + + foreach (List group in grouped) + { + //Another round of sending the colors if we are at the limit + if (counter >= 255) + { + await Post(new StateRequest { On = true, Segments = [new() { Id = 0, IndividualLedControl = [.. list] }] }); + list = []; + counter = 0; + } + + // If there is only one LED in the group, add it to the list + if (group.Count == 1) + { + list.Add(group.First()); + list.Add(leds.First().Color); + counter++; + continue; + } + + //And if there are multiple LED's, Add them to the list, but when displaying max + //is not displayed so add 1 to the max to get it to display properly + list.Add(group.Min()); + list.Add(group.Max() + 1); + list.Add(leds.First().Color); + counter++; + } } - await Post(new StateRequest { Segments = [new() { Id = 0, IndividualLedControl = [.. list] }] }); + //And finally send the last packet + await Post(new StateRequest { On = true, Segments = [new() { Id = 0, IndividualLedControl = [.. list] }] }); } } From 987a90a80d63cb85d8786d89fb6b4465fb429541 Mon Sep 17 00:00:00 2001 From: Jrdiver Date: Fri, 27 Dec 2024 22:07:41 -0600 Subject: [PATCH 3/6] Add some json fileds that were missing and rename single led to match naming scheme --- src/Kevsoft.WLED/InformationResponse.cs | 20 +++++++++---------- src/Kevsoft.WLED/LedsResponse.cs | 12 +++++++++++ src/Kevsoft.WLED/MatrixResponse.cs | 12 +++++++++++ .../{SingleLed.cs => SingleLedRequest.cs} | 2 +- src/Kevsoft.WLED/WLedClient.cs | 4 ++-- 5 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 src/Kevsoft.WLED/MatrixResponse.cs rename src/Kevsoft.WLED/{SingleLed.cs => SingleLedRequest.cs} (88%) diff --git a/src/Kevsoft.WLED/InformationResponse.cs b/src/Kevsoft.WLED/InformationResponse.cs index 35c4a0c..796565a 100644 --- a/src/Kevsoft.WLED/InformationResponse.cs +++ b/src/Kevsoft.WLED/InformationResponse.cs @@ -6,7 +6,7 @@ public sealed class InformationResponse /// Version name. /// [JsonPropertyName("ver")] - public string VersionName { get; set; } = null!; + public string? VersionName { get; set; } = null; /// /// Build ID (YYMMDDB, B = daily build index). @@ -18,7 +18,7 @@ public sealed class InformationResponse /// LEDs Information /// [JsonPropertyName("leds")] - public LedsResponse Leds { get; set; } = null!; + public LedsResponse? Leds { get; set; } = null; /// /// If true, an UI with only a single button for toggling sync should toggle receive+send, otherwise send only @@ -30,7 +30,7 @@ public sealed class InformationResponse /// Friendly name of the light. /// [JsonPropertyName("name")] - public string Name { get; set; } = null!; + public string? Name { get; set; } = null; /// /// The UDP port for realtime packets and WLED broadcast. @@ -60,13 +60,13 @@ public sealed class InformationResponse /// Name of the platform. /// [JsonPropertyName("arch")] - public string Arch { get; set; } = null!; + public string? Arch { get; set; } = null; /// /// Version of the underlying (Arduino core) SDK. /// [JsonPropertyName("core")] - public string Core { get; set; } = null!; + public string? Core { get; set; } = null; /// /// Bytes of heap memory (RAM) currently available. Problematic if less than 10k. @@ -90,29 +90,29 @@ public sealed class InformationResponse /// The producer/vendor of the light. Always WLED for standard installations. /// [JsonPropertyName("brand")] - public string Brand { get; set; } = null!; + public string? Brand { get; set; } = null; /// /// The product name. Always FOSS for standard installations. /// [JsonPropertyName("product")] - public string Product { get; set; } = null!; + public string? Product { get; set; } = null; /// /// The origin of the build. src if a release version is compiled from source, bin for an official release image, dev for a development build (regardless of src/bin origin) and exp for experimental versions. ogn if the image is flashed to hardware by the vendor. /// [JsonPropertyName("btype")] - public string BuildType { get; set; } = null!; + public string? BuildType { get; set; } = null; /// /// The hexadecimal hardware MAC address of the light, lowercase and without colons. /// [JsonPropertyName("mac")] - public string MacAddress { get; set; } = null!; + public string? MacAddress { get; set; } = null; /// /// The IP address of this instance. Empty string if not connected. (since 0.13.0) /// [JsonPropertyName("ip")] - public string NetworkAddress { get; set; } = null!; + public string? NetworkAddress { get; set; } = null; } \ No newline at end of file diff --git a/src/Kevsoft.WLED/LedsResponse.cs b/src/Kevsoft.WLED/LedsResponse.cs index dd0c0eb..e0247f6 100644 --- a/src/Kevsoft.WLED/LedsResponse.cs +++ b/src/Kevsoft.WLED/LedsResponse.cs @@ -37,4 +37,16 @@ public sealed class LedsResponse /// [JsonPropertyName("maxseg")] public byte MaximumSegments { get; set; } + + /// + /// Preset number loaded on boot. + /// + [JsonPropertyName("bootps")] + public int BootupPreset { get; set; } + + /// + /// Matrix configuration + /// + [JsonPropertyName("matrix")] + public MatrixResponse? Matrix { get; set; } = null; } \ No newline at end of file diff --git a/src/Kevsoft.WLED/MatrixResponse.cs b/src/Kevsoft.WLED/MatrixResponse.cs new file mode 100644 index 0000000..a227456 --- /dev/null +++ b/src/Kevsoft.WLED/MatrixResponse.cs @@ -0,0 +1,12 @@ +namespace Kevsoft.WLED; + +public class MatrixResponse +{ + /// The number of LEDs in the width of the matrix + [JsonPropertyName("w")] + public int Width { get; set; } + + /// The number of LEDs in the Height of the matrix + [JsonPropertyName("h")] + public int Height { get; set; } +} \ No newline at end of file diff --git a/src/Kevsoft.WLED/SingleLed.cs b/src/Kevsoft.WLED/SingleLedRequest.cs similarity index 88% rename from src/Kevsoft.WLED/SingleLed.cs rename to src/Kevsoft.WLED/SingleLedRequest.cs index 8de04ab..b04e807 100644 --- a/src/Kevsoft.WLED/SingleLed.cs +++ b/src/Kevsoft.WLED/SingleLedRequest.cs @@ -1,6 +1,6 @@ namespace Kevsoft.WLED; -public sealed class SingleLed +public sealed class SingleLedRequest { /// The position of the LED in the segment public int LedPosition { get; set; } diff --git a/src/Kevsoft.WLED/WLedClient.cs b/src/Kevsoft.WLED/WLedClient.cs index b786df8..4e7618c 100644 --- a/src/Kevsoft.WLED/WLedClient.cs +++ b/src/Kevsoft.WLED/WLedClient.cs @@ -83,7 +83,7 @@ public async Task Post(StateRequest request) result.EnsureSuccessStatusCode(); } - public async Task Post(List ledList) + public async Task Post(List ledList) { // Eliminate duplicate positions ledList = ledList.GroupBy(x => x.LedPosition).Select(x => x.Last()).ToList(); @@ -92,7 +92,7 @@ public async Task Post(List ledList) int counter = 0; //Attempt to group colors together to reduce the number of packets sent as there is a 256 color at a time limit - foreach (IGrouping? leds in ledList.GroupBy(x => x.Color)) + foreach (IGrouping? leds in ledList.GroupBy(x => x.Color)) { if (counter >=255) { From 78d977007b9aa020403d0069ec0c8cf2aa63a8b6 Mon Sep 17 00:00:00 2001 From: Jrdiver Date: Sat, 25 Jan 2025 13:00:18 -0600 Subject: [PATCH 4/6] Undo Uneeded Change --- src/Kevsoft.WLED/InformationResponse.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kevsoft.WLED/InformationResponse.cs b/src/Kevsoft.WLED/InformationResponse.cs index 796565a..b5600fc 100644 --- a/src/Kevsoft.WLED/InformationResponse.cs +++ b/src/Kevsoft.WLED/InformationResponse.cs @@ -6,7 +6,7 @@ public sealed class InformationResponse /// Version name. /// [JsonPropertyName("ver")] - public string? VersionName { get; set; } = null; + public string VersionName { get; set; } = null!; /// /// Build ID (YYMMDDB, B = daily build index). From 1407723518975ba472c51b7e193d8b317b7cdc0a Mon Sep 17 00:00:00 2001 From: Jrdiver Date: Tue, 11 Feb 2025 12:28:26 -0600 Subject: [PATCH 5/6] More Unnesssary nullables and fix existing tests due to new properties --- src/Kevsoft.WLED/InformationResponse.cs | 18 +++++++++--------- src/Kevsoft.WLED/LedsResponse.cs | 2 +- test/Kevsoft.WLED.Tests/JsonBuilder.cs | 9 +++++++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Kevsoft.WLED/InformationResponse.cs b/src/Kevsoft.WLED/InformationResponse.cs index b5600fc..35c4a0c 100644 --- a/src/Kevsoft.WLED/InformationResponse.cs +++ b/src/Kevsoft.WLED/InformationResponse.cs @@ -18,7 +18,7 @@ public sealed class InformationResponse /// LEDs Information /// [JsonPropertyName("leds")] - public LedsResponse? Leds { get; set; } = null; + public LedsResponse Leds { get; set; } = null!; /// /// If true, an UI with only a single button for toggling sync should toggle receive+send, otherwise send only @@ -30,7 +30,7 @@ public sealed class InformationResponse /// Friendly name of the light. /// [JsonPropertyName("name")] - public string? Name { get; set; } = null; + public string Name { get; set; } = null!; /// /// The UDP port for realtime packets and WLED broadcast. @@ -60,13 +60,13 @@ public sealed class InformationResponse /// Name of the platform. /// [JsonPropertyName("arch")] - public string? Arch { get; set; } = null; + public string Arch { get; set; } = null!; /// /// Version of the underlying (Arduino core) SDK. /// [JsonPropertyName("core")] - public string? Core { get; set; } = null; + public string Core { get; set; } = null!; /// /// Bytes of heap memory (RAM) currently available. Problematic if less than 10k. @@ -90,29 +90,29 @@ public sealed class InformationResponse /// The producer/vendor of the light. Always WLED for standard installations. /// [JsonPropertyName("brand")] - public string? Brand { get; set; } = null; + public string Brand { get; set; } = null!; /// /// The product name. Always FOSS for standard installations. /// [JsonPropertyName("product")] - public string? Product { get; set; } = null; + public string Product { get; set; } = null!; /// /// The origin of the build. src if a release version is compiled from source, bin for an official release image, dev for a development build (regardless of src/bin origin) and exp for experimental versions. ogn if the image is flashed to hardware by the vendor. /// [JsonPropertyName("btype")] - public string? BuildType { get; set; } = null; + public string BuildType { get; set; } = null!; /// /// The hexadecimal hardware MAC address of the light, lowercase and without colons. /// [JsonPropertyName("mac")] - public string? MacAddress { get; set; } = null; + public string MacAddress { get; set; } = null!; /// /// The IP address of this instance. Empty string if not connected. (since 0.13.0) /// [JsonPropertyName("ip")] - public string? NetworkAddress { get; set; } = null; + public string NetworkAddress { get; set; } = null!; } \ No newline at end of file diff --git a/src/Kevsoft.WLED/LedsResponse.cs b/src/Kevsoft.WLED/LedsResponse.cs index e0247f6..36e311d 100644 --- a/src/Kevsoft.WLED/LedsResponse.cs +++ b/src/Kevsoft.WLED/LedsResponse.cs @@ -48,5 +48,5 @@ public sealed class LedsResponse /// Matrix configuration /// [JsonPropertyName("matrix")] - public MatrixResponse? Matrix { get; set; } = null; + public MatrixResponse Matrix { get; set; } = null!; } \ No newline at end of file diff --git a/test/Kevsoft.WLED.Tests/JsonBuilder.cs b/test/Kevsoft.WLED.Tests/JsonBuilder.cs index c83a1fd..b377aa8 100644 --- a/test/Kevsoft.WLED.Tests/JsonBuilder.cs +++ b/test/Kevsoft.WLED.Tests/JsonBuilder.cs @@ -60,8 +60,13 @@ public static string CreateInformationJson(InformationResponse information) ""lc"": {information.Leds.LightCapabilities}, ""pwr"": {information.Leds.PowerUsage}, ""maxpwr"": {information.Leds.MaximumPower}, - ""maxseg"": {information.Leds.MaximumSegments} - }}, + ""maxseg"": {information.Leds.MaximumSegments}, + ""bootps"": {information.Leds.BootupPreset}, + ""matrix"": {{ + ""w"": {information.Leds.Matrix.Width}, + ""h"": {information.Leds.Matrix.Height} + }} + }}, ""str"": {information.ToggleSendReceive.ToString().ToLower()}, ""name"": ""{information.Name}"", ""udpport"": {information.UdpPort}, From 017d0b57c028c6afc189738bdf0164bab4198fc2 Mon Sep 17 00:00:00 2001 From: Jrdiver Date: Wed, 12 Feb 2025 00:02:47 -0600 Subject: [PATCH 6/6] Added Test. --- .../Kevsoft.WLED.Tests/WLedClientPostTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/Kevsoft.WLED.Tests/WLedClientPostTests.cs b/test/Kevsoft.WLED.Tests/WLedClientPostTests.cs index 274bab5..a80ef3d 100644 --- a/test/Kevsoft.WLED.Tests/WLedClientPostTests.cs +++ b/test/Kevsoft.WLED.Tests/WLedClientPostTests.cs @@ -38,6 +38,29 @@ public async Task PostEmptyStateRequestData() json.RootElement.EnumerateObject().Should().HaveCount(0); } + [Fact] + public async Task PostEmptySingleLedRequestData() + { + var mockHttpMessageHandler = new MockHttpMessageHandler(); + var baseUri = $"http://{Guid.NewGuid():N}.com"; + mockHttpMessageHandler.AppendResponse($"{baseUri}/json/state"); + var client = new WLedClient(mockHttpMessageHandler, baseUri); + + List request = []; + await client.Post(request); + + var (uri, body) = mockHttpMessageHandler.CapturedRequests.Single(); + uri.Should().Be($"{baseUri}/json/state"); + var json = JsonDocument.Parse(body!); + // Expected Request: + // {"on":true,"seg":[{"id":0,"i":[]}]} + + json.RootElement.EnumerateObject().Should().HaveCount(2); + json.RootElement.GetProperty("seg").EnumerateArray().Should().HaveCount(1); + json.RootElement.GetProperty("seg")[0].GetProperty("id").GetInt32().Should().Be(0); + json.RootElement.GetProperty("seg")[0].GetProperty("i").EnumerateArray().Should().HaveCount(0); + } + [Fact] public async Task PostFullWLedRootResponse() {