From 6fac7e2127f84a73997f95c6281a84a1ad9b85aa Mon Sep 17 00:00:00 2001 From: parsons Date: Fri, 7 Aug 2026 16:05:32 +0100 Subject: [PATCH] Add project files. --- MatrixRenderer.cs | 60 +++++++++++++++++++++++++++++++++++++ Program.cs | 13 ++++++++ WLED_DDP_Client.csproj | 15 ++++++++++ WLED_DDP_Client.slnx | 3 ++ WledDdpClient.cs | 68 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 159 insertions(+) create mode 100644 MatrixRenderer.cs create mode 100644 Program.cs create mode 100644 WLED_DDP_Client.csproj create mode 100644 WLED_DDP_Client.slnx create mode 100644 WledDdpClient.cs diff --git a/MatrixRenderer.cs b/MatrixRenderer.cs new file mode 100644 index 0000000..05a6652 --- /dev/null +++ b/MatrixRenderer.cs @@ -0,0 +1,60 @@ +using SkiaSharp; +using System.Threading.Tasks; + +public class MatrixRenderer +{ + private readonly WledDdpClient _wled; + private readonly int _width = 64; + private readonly int _height = 64; + + public MatrixRenderer(string wledIp) + { + _wled = new WledDdpClient(wledIp); + } + + public async Task DrawAndSendFrameAsync(float currentTemp) + { + // 1. Create a 64x64 canvas + using SKBitmap bitmap = new SKBitmap(_width, _height, SKColorType.Rgba8888, SKAlphaType.Premul); + using SKCanvas canvas = new SKCanvas(bitmap); + + // 2. Clear background to black + canvas.Clear(SKColors.Black); + + // 3. Draw something (e.g., Temperature Text) + + // Paint now only handles the color and rendering style + using SKPaint textPaint = new SKPaint + { + Color = SKColors.Orange, + IsAntialias = false // Keep crisp pixels for LED matrices + }; + + // Font handles the typography + using SKFont textFont = new SKFont + { + Size = 16 + }; + + // DrawText now takes the string, X, Y, the font, and the paint + //canvas.DrawText($"{currentTemp:F1}c", 4, 32, textFont, textPaint); + // DrawText now takes the string, X, Y, alignment, the font, and the paint + canvas.DrawText($"{currentTemp:F1}c", 4, 32, SKTextAlign.Left, textFont, textPaint); + + // 4. Convert 32-bit RGBA to 24-bit RGB for WLED + byte[] rgbaBytes = bitmap.Bytes; + byte[] rgbBytes = new byte[_width * _height * 3]; + + int rgbIndex = 0; + for (int i = 0; i < rgbaBytes.Length; i += 4) + { + rgbBytes[rgbIndex++] = rgbaBytes[i]; // R + rgbBytes[rgbIndex++] = rgbaBytes[i + 1]; // G + rgbBytes[rgbIndex++] = rgbaBytes[i + 2]; // B + // We ignore rgbaBytes[i + 3] (Alpha channel) + } + + // 5. Fire it over UDP! + await _wled.SendFrameAsync(rgbBytes); + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..5b700b6 --- /dev/null +++ b/Program.cs @@ -0,0 +1,13 @@ +var renderer = new MatrixRenderer("192.168.0.150"); // Replace with WLED IP + +while (true) +{ + // TODO: Query InfluxDB for your BME680 data here + float bme680Temp = 22.5f; + + await renderer.DrawAndSendFrameAsync(bme680Temp); + + // Update screen at 30 FPS for smooth transitions, + // or every 5 seconds if just showing static data. + await Task.Delay(1000 / 30); +} \ No newline at end of file diff --git a/WLED_DDP_Client.csproj b/WLED_DDP_Client.csproj new file mode 100644 index 0000000..8c278a7 --- /dev/null +++ b/WLED_DDP_Client.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + diff --git a/WLED_DDP_Client.slnx b/WLED_DDP_Client.slnx new file mode 100644 index 0000000..c29e2ba --- /dev/null +++ b/WLED_DDP_Client.slnx @@ -0,0 +1,3 @@ + + + diff --git a/WledDdpClient.cs b/WledDdpClient.cs new file mode 100644 index 0000000..03d8b25 --- /dev/null +++ b/WledDdpClient.cs @@ -0,0 +1,68 @@ +using System; +using System.Net.Sockets; +using System.Threading.Tasks; + +public class WledDdpClient +{ + private readonly UdpClient _udp; + private readonly string _ip; + private const int Port = 4048; // Standard DDP UDP port + private byte _sequence = 1; + + public WledDdpClient(string ipAddress) + { + _ip = ipAddress; + _udp = new UdpClient(); + } + + public async Task SendFrameAsync(byte[] rgbData) + { + // Max payload per packet to stay under standard 1500 byte MTU + const int maxPixelsPerPacket = 480; + const int maxBytesPerPacket = maxPixelsPerPacket * 3; + + int totalBytes = rgbData.Length; + int offset = 0; + + while (offset < totalBytes) + { + int chunkLength = Math.Min(maxBytesPerPacket, totalBytes - offset); + bool isLast = (offset + chunkLength) >= totalBytes; + + byte[] packet = new byte[10 + chunkLength]; + + // Byte 0: Flags. 0x40 = Version 1. 0x01 = Push bit. + // We only "Push" to the display on the final chunk of the frame. + packet[0] = (byte)(isLast ? 0x41 : 0x40); + + // Byte 1: Sequence (0-15) + packet[1] = _sequence; + + // Byte 2: Data type (1 = RGB) + packet[2] = 1; + + // Byte 3: Destination ID (Default 1) + packet[3] = 1; + + // Bytes 4-7: Data Offset (32-bit Big Endian) + packet[4] = (byte)(offset >> 24); + packet[5] = (byte)(offset >> 16); + packet[6] = (byte)(offset >> 8); + packet[7] = (byte)(offset); + + // Bytes 8-9: Data Length (16-bit Big Endian) + packet[8] = (byte)(chunkLength >> 8); + packet[9] = (byte)(chunkLength); + + // Copy the pixel payload into the packet + Buffer.BlockCopy(rgbData, offset, packet, 10, chunkLength); + + await _udp.SendAsync(packet, packet.Length, _ip, Port); + + offset += chunkLength; + } + + // Increment sequence for the next frame (wrap at 15) + _sequence = (byte)((_sequence + 1) % 16); + } +} \ No newline at end of file