From 7c6db8738c58c11777cc4fe105f83c6a74d04db2 Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Sun, 12 Jul 2020 16:06:54 +0200 Subject: [PATCH 1/4] Added device-provider for NodeMCU based WS281X-leds --- .../NodeMCU/NodeMCUWS2812USBDevice.cs | 87 ++++++++ .../NodeMCU/NodeMCUWS2812USBDeviceInfo.cs | 51 +++++ .../NodeMCU/NodeMCUWS2812USBUpdateQueue.cs | 106 +++++++++ .../NodeMCU/NodeMCUWS281XDeviceDefinition.cs | 71 ++++++ .../Sketches/RGB.NET_udp.ino | 210 ++++++++++++++++++ 5 files changed, 525 insertions(+) create mode 100644 RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBDevice.cs create mode 100644 RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBDeviceInfo.cs create mode 100644 RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBUpdateQueue.cs create mode 100644 RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS281XDeviceDefinition.cs create mode 100644 RGB.NET.Devices.WS281X/Sketches/RGB.NET_udp.ino diff --git a/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBDevice.cs b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBDevice.cs new file mode 100644 index 0000000..4b03b81 --- /dev/null +++ b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBDevice.cs @@ -0,0 +1,87 @@ +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedMember.Global + +using System.Collections.Generic; +using System.Linq; +using RGB.NET.Core; + +namespace RGB.NET.Devices.WS281X.NodeMCU +{ + // ReSharper disable once InconsistentNaming + /// + /// + /// Represents an NodeMCU WS2812 device. + /// + public class NodeMCUWS2812USBDevice : AbstractRGBDevice, ILedStripe + { + #region Properties & Fields + + /// + /// Gets the update queue performing updates for this device. + /// + public NodeMCUWS2812USBUpdateQueue UpdateQueue { get; } + + /// + public override NodeMCUWS2812USBDeviceInfo DeviceInfo { get; } + + /// + /// Gets the channel (as defined in the NodeMCU-sketch) this device is attached to. + /// + public int Channel { get; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The update trigger used by this queue. + /// The update queue performing updates for this device. + /// The channel (as defined in the NodeMCU-sketch) this device is attached to. + public NodeMCUWS2812USBDevice(NodeMCUWS2812USBDeviceInfo deviceInfo, NodeMCUWS2812USBUpdateQueue updateQueue, int channel) + { + this.DeviceInfo = deviceInfo; + this.UpdateQueue = updateQueue; + this.Channel = channel; + } + + #endregion + + #region Methods + + internal void Initialize(int ledCount) + { + for (int i = 0; i < ledCount; i++) + InitializeLed(LedId.LedStripe1 + i, new Point(i * 10, 0), new Size(10, 10)); + + //TODO DarthAffe 23.12.2018: Allow to load a layout. + + if (Size == Size.Invalid) + { + Rectangle ledRectangle = new Rectangle(this.Select(x => x.LedRectangle)); + Size = ledRectangle.Size + new Size(ledRectangle.Location.X, ledRectangle.Location.Y); + } + } + + /// + protected override object CreateLedCustomData(LedId ledId) => (Channel, (int)ledId - (int)LedId.LedStripe1); + + /// + protected override IEnumerable GetLedsToUpdate(bool flushLeds) => (flushLeds || LedMapping.Values.Any(x => x.IsDirty)) ? LedMapping.Values : null; + + /// + protected override void UpdateLeds(IEnumerable ledsToUpdate) => UpdateQueue.SetData(ledsToUpdate.Where(x => x.Color.A > 0)); + + /// + public override void Dispose() + { + try { UpdateQueue?.Dispose(); } + catch { /* at least we tried */ } + + base.Dispose(); + } + + #endregion + } +} diff --git a/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBDeviceInfo.cs b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBDeviceInfo.cs new file mode 100644 index 0000000..916f516 --- /dev/null +++ b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBDeviceInfo.cs @@ -0,0 +1,51 @@ +using System; +using RGB.NET.Core; + +namespace RGB.NET.Devices.WS281X.NodeMCU +{ + // ReSharper disable once InconsistentNaming + /// + /// + /// Represents a generic information for a . + /// + public class NodeMCUWS2812USBDeviceInfo : IRGBDeviceInfo + { + #region Properties & Fields + + /// + public string DeviceName { get; } + + /// + public RGBDeviceType DeviceType => RGBDeviceType.LedStripe; + + /// + public string Manufacturer => "NodeMCU"; + + /// + public string Model => "WS2812 WLAN"; + + /// + public RGBDeviceLighting Lighting => RGBDeviceLighting.Key; + + /// + public bool SupportsSyncBack => false; + + /// + public Uri Image { get; set; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The name of this device. + public NodeMCUWS2812USBDeviceInfo(string name) + { + this.DeviceName = name; + } + + #endregion + } +} diff --git a/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBUpdateQueue.cs b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBUpdateQueue.cs new file mode 100644 index 0000000..cb4424e --- /dev/null +++ b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBUpdateQueue.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using RGB.NET.Core; + +namespace RGB.NET.Devices.WS281X.NodeMCU +{ + // ReSharper disable once InconsistentNaming + /// + /// + /// Represents the update-queue performing updates for NodeMCU WS2812 devices. + /// + public class NodeMCUWS2812USBUpdateQueue : UpdateQueue + { + #region Constants + + private static readonly byte UPDATE_COMMAND = 0x02; + + #endregion + + #region Properties & Fields + + private readonly string _hostname; + + private readonly UdpClient _udpClient; + private readonly Dictionary _dataBuffer = new Dictionary(); + private readonly Dictionary _sequenceNumbers = new Dictionary(); + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The update trigger used by this queue. + /// The hostname to connect to. + /// The port used by the web-connection. + public NodeMCUWS2812USBUpdateQueue(IDeviceUpdateTrigger updateTrigger, string hostname, int port) + : base(updateTrigger) + { + this._hostname = hostname; + + _udpClient = new UdpClient(_hostname, port); + _udpClient.Connect(_hostname, port); + } + + #endregion + + #region Methods + + /// + protected override void Update(Dictionary dataSet) + { + foreach (IGrouping channelData in dataSet.Select(x => (((int channel, int key))x.Key, x.Value)) + .GroupBy(x => x.Item1.channel)) + { + int channel = channelData.Key; + byte[] buffer = _dataBuffer[channel]; + + buffer[0] = GetSequenceNumber(channel); + buffer[1] = (byte)((channel << 4) | UPDATE_COMMAND); + int i = 2; + foreach ((byte _, byte r, byte g, byte b) in channelData.OrderBy(x => x.Item1.key) + .Select(x => x.Value.GetRGBBytes())) + { + buffer[i++] = r; + buffer[i++] = g; + buffer[i++] = b; + } + + Send(buffer); + } + } + + internal IEnumerable<(int channel, int ledCount)> GetChannels() + { + WebClient webClient = new WebClient(); + webClient.DownloadString($"http://{_hostname}/reset"); + + int channelCount = int.Parse(webClient.DownloadString($"http://{_hostname}/channels")); + for (int channel = 1; channel <= channelCount; channel++) + { + int ledCount = int.Parse(webClient.DownloadString($"http://{_hostname}/channel/{channel}")); + if (ledCount > 0) + { + _dataBuffer[channel] = new byte[(ledCount * 3) + 2]; + _sequenceNumbers[channel] = 0; + yield return (channel, ledCount); + } + } + } + + private void Send(byte[] data) => _udpClient.Send(data, data.Length); + + private byte GetSequenceNumber(int channel) + { + byte sequenceNumber = (byte)((_sequenceNumbers[channel] + 1) % byte.MaxValue); + _sequenceNumbers[channel] = sequenceNumber; + return sequenceNumber; + } + + #endregion + } +} diff --git a/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS281XDeviceDefinition.cs b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS281XDeviceDefinition.cs new file mode 100644 index 0000000..14d5a05 --- /dev/null +++ b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS281XDeviceDefinition.cs @@ -0,0 +1,71 @@ +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global + +using System.Collections.Generic; +using RGB.NET.Core; + +namespace RGB.NET.Devices.WS281X.NodeMCU +{ + // ReSharper disable once InconsistentNaming + /// + /// + /// Represents a definition of an NodeMCU WS2812 devices. + /// + public class NodeMCUWS281XDeviceDefinition : IWS281XDeviceDefinition + { + #region Properties & Fields + + /// + /// Gets the hostname to connect to. + /// + public string Hostname { get; } + + /// + /// Gets the port of the UDP connection. + /// + public int Port { get; } + + /// + /// Gets or sets the name used by this device. + /// This allows to use {0} as a placeholder for a incrementing number if multiple devices are created. + /// + public string Name { get; set; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The hostname to connect to. + /// The port of the UDP connection. + public NodeMCUWS281XDeviceDefinition(string hostname, int port = 1872) + { + this.Hostname = hostname; + this.Port = port; + } + + #endregion + + #region Methods + + /// + public IEnumerable CreateDevices(IDeviceUpdateTrigger updateTrigger) + { + NodeMCUWS2812USBUpdateQueue queue = new NodeMCUWS2812USBUpdateQueue(updateTrigger, Hostname, Port); + IEnumerable<(int channel, int ledCount)> channels = queue.GetChannels(); + int counter = 0; + foreach ((int channel, int ledCount) in channels) + { + string name = string.Format(Name ?? $"NodeMCU WS2812 WIFI ({Hostname}) [{{0}}]", ++counter); + NodeMCUWS2812USBDevice device = new NodeMCUWS2812USBDevice(new NodeMCUWS2812USBDeviceInfo(name), queue, channel); + device.Initialize(ledCount); + yield return device; + } + } + + #endregion + } +} diff --git a/RGB.NET.Devices.WS281X/Sketches/RGB.NET_udp.ino b/RGB.NET.Devices.WS281X/Sketches/RGB.NET_udp.ino new file mode 100644 index 0000000..91f03d2 --- /dev/null +++ b/RGB.NET.Devices.WS281X/Sketches/RGB.NET_udp.ino @@ -0,0 +1,210 @@ +#define FASTLED_ESP8266_RAW_PIN_ORDER + +#include "FastLED.h" +#include +#include + +//#### CONFIGURATION #### + +// WLAN settings +const char* ssid = ""; // WLAN-network-name +const char* password = ""; // WLAN-password + +#define CHANNELS 4 // change this only if you add or remove channels in the implementation-part. To disable channels set them to 0 leds. + +// should not exceed 168 leds, since that results in the maximum paket size that is safe to transmit. Everything above could potentially be dropped. +// no more than 255 leds per channel (hard limit) +#define LEDS_CHANNEL_1 3 +#define LEDS_CHANNEL_2 0 +#define LEDS_CHANNEL_3 0 +#define LEDS_CHANNEL_4 0 + +#define PIN_CHANNEL_1 15 // D8 +#define PIN_CHANNEL_2 13 // D7 +#define PIN_CHANNEL_3 12 // D6 +#define PIN_CHANNEL_4 14 // D5 + +#define PORT 1872 // if changed needs to be configured in RGB.NET (default: 1872) + +#define WEBSERVER_PORT 80 + +//####################### + +CRGB leds_channel_1[LEDS_CHANNEL_1]; +CRGB leds_channel_2[LEDS_CHANNEL_2]; +CRGB leds_channel_3[LEDS_CHANNEL_3]; +CRGB leds_channel_4[LEDS_CHANNEL_4]; + +WiFiServer server(WEBSERVER_PORT); +WiFiUDP Udp; + +byte incomingPacket[767]; // 255 (max leds) * 3 + 2 (header) +byte lastSequenceNumbers[CHANNELS]; + +String header; + +void setup() { + if(LEDS_CHANNEL_1 > 0) { FastLED.addLeds(leds_channel_1, LEDS_CHANNEL_1); } + if(LEDS_CHANNEL_2 > 0) { FastLED.addLeds(leds_channel_2, LEDS_CHANNEL_2); } + if(LEDS_CHANNEL_3 > 0) { FastLED.addLeds(leds_channel_3, LEDS_CHANNEL_3); } + if(LEDS_CHANNEL_4 > 0) { FastLED.addLeds(leds_channel_4, LEDS_CHANNEL_4); } + + for(int i = 0; i < CHANNELS; i++) + { + lastSequenceNumbers[i] = 255; + } + + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + } + delay(100); + + Udp.begin(PORT); + server.begin(); +} + +bool checkSequenceNumber(int channel, byte currentSequenceNumber) +{ + bool isValid = (currentSequenceNumber > lastSequenceNumbers[channel]) || ((lastSequenceNumbers[channel] > 200) && (currentSequenceNumber < 50)); + if(isValid) + { + lastSequenceNumbers[channel] = currentSequenceNumber; + } + return isValid; +} + +void loop() { + // Web client + WiFiClient client = server.available(); + if (client) + { + String currentLine = ""; + while (client.connected()) + { + if (client.available()) + { + char c = client.read(); + header += c; + if (c == '\n') { + if (currentLine.length() == 0) { + client.println("HTTP/1.1 200 OK"); + client.println("Content-type:text/html"); + client.println("Connection: close"); + client.println(); + + if (header.indexOf("GET /reset") >= 0) + { + for(int i = 0; i < CHANNELS; i++) + { + lastSequenceNumbers[i] = 255; + } + for(int i = 0; i < LEDS_CHANNEL_1; i++) + { + leds_channel_1[i] = CRGB::Black; + } + for(int i = 0; i < LEDS_CHANNEL_2; i++) + { + leds_channel_2[i] = CRGB::Black; + } + for(int i = 0; i < LEDS_CHANNEL_3; i++) + { + leds_channel_3[i] = CRGB::Black; + } + for(int i = 0; i < LEDS_CHANNEL_4; i++) + { + leds_channel_4[i] = CRGB::Black; + } + FastLED.show(); + } + else if (header.indexOf("GET /channels") >= 0) + { + client.println(CHANNELS); + } + else if (header.indexOf("GET /channel/1") >= 0) + { + client.println(LEDS_CHANNEL_1); + } + else if (header.indexOf("GET /channel/2") >= 0) + { + client.println(LEDS_CHANNEL_2); + } + else if (header.indexOf("GET /channel/3") >= 0) + { + client.println(LEDS_CHANNEL_3); + } + else if (header.indexOf("GET /channel/4") >= 0) + { + client.println(LEDS_CHANNEL_4); + } + client.println(); + + break; + } + else + { + currentLine = ""; + } + } + else if (c != '\r') + { + currentLine += c; + } + } + } + header = ""; + client.stop(); + } + + // Color update + int packetSize = Udp.parsePacket(); + if (packetSize) + { + // receive incoming UDP packets + byte sequenceNumber = Udp.read(); + byte command = Udp.read(); + switch(command) + { + // ### channel 1 ### + case 0x12: // set leds of channel 1 + if(checkSequenceNumber(0, sequenceNumber)) + { + Udp.read(((uint8_t*)leds_channel_1), (LEDS_CHANNEL_1 * 3)); + FastLED.show(); + } + break; + + // ### channel 2 ### + case 0x22: // set leds of channel 2 + if(checkSequenceNumber(1, sequenceNumber)) + { + Udp.read(((uint8_t*)leds_channel_2), (LEDS_CHANNEL_2 * 3)); + FastLED.show(); + } + break; + + // ### channel 3 ### + case 0x32: // set leds of channel 3 + if(checkSequenceNumber(2, sequenceNumber)) + { + Udp.read(((uint8_t*)leds_channel_3), (LEDS_CHANNEL_3 * 3)); + FastLED.show(); + } + break; + + // ### channel 4 ### + case 0x42: // set leds of channel 4 + if(checkSequenceNumber(3, sequenceNumber)) + { + Udp.read(((uint8_t*)leds_channel_4), (LEDS_CHANNEL_4 * 3)); + FastLED.show(); + } + break; + + // ### default ### + default: + break; + } + } +} From dd5fb6db890f30e214f29625f8e1c42604f8caaf Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Mon, 13 Jul 2020 23:42:07 +0200 Subject: [PATCH 2/4] Changed UpdateQueue.Dispose to be virtual --- RGB.NET.Core/Update/Devices/UpdateQueue.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RGB.NET.Core/Update/Devices/UpdateQueue.cs b/RGB.NET.Core/Update/Devices/UpdateQueue.cs index 05aff8d..da5c82c 100644 --- a/RGB.NET.Core/Update/Devices/UpdateQueue.cs +++ b/RGB.NET.Core/Update/Devices/UpdateQueue.cs @@ -100,7 +100,8 @@ namespace RGB.NET.Core _currentDataSet = null; } - public void Dispose() + /// + public virtual void Dispose() { _updateTrigger.Starting -= OnStartup; _updateTrigger.Update -= OnUpdate; From b8a3593792761ca74f43613af1647bb6d33603f2 Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Mon, 13 Jul 2020 23:52:08 +0200 Subject: [PATCH 3/4] Added a REST-API to NodeMCU devices and added using it as an optional update-mode --- .../NodeMCU/NodeMCUUpdateMode.cs | 21 ++ .../NodeMCU/NodeMCUWS2812USBUpdateQueue.cs | 154 ++++++--- .../NodeMCU/NodeMCUWS281XDeviceDefinition.cs | 24 +- .../RGB.NET.Devices.WS281X.csproj | 1 + .../Sketches/RGB.NET_NodeMCU.ino | 295 ++++++++++++++++++ .../Sketches/RGB.NET_udp.ino | 210 ------------- .../WS281XDeviceProvider.cs | 7 +- 7 files changed, 457 insertions(+), 255 deletions(-) create mode 100644 RGB.NET.Devices.WS281X/NodeMCU/NodeMCUUpdateMode.cs create mode 100644 RGB.NET.Devices.WS281X/Sketches/RGB.NET_NodeMCU.ino delete mode 100644 RGB.NET.Devices.WS281X/Sketches/RGB.NET_udp.ino diff --git a/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUUpdateMode.cs b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUUpdateMode.cs new file mode 100644 index 0000000..f20bd8e --- /dev/null +++ b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUUpdateMode.cs @@ -0,0 +1,21 @@ +namespace RGB.NET.Devices.WS281X.NodeMCU +{ + /// + /// Contaisn a list of possible update-modes for NodeMCU-devices. + /// + // ReSharper disable once InconsistentNaming + public enum NodeMCUUpdateMode + { + /// + /// Updates through the HTTP-REST-API. + /// Slow, but reliable. + /// + Http, + + /// + /// Updates through a UDP-connection. + /// Fast, but might skip updates if the network connection is bad. + /// + Udp + } +} diff --git a/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBUpdateQueue.cs b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBUpdateQueue.cs index cb4424e..f5feffe 100644 --- a/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBUpdateQueue.cs +++ b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBUpdateQueue.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using System.Net; +using System.Net.Http; using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; using RGB.NET.Core; namespace RGB.NET.Devices.WS281X.NodeMCU @@ -13,94 +16,169 @@ namespace RGB.NET.Devices.WS281X.NodeMCU /// public class NodeMCUWS2812USBUpdateQueue : UpdateQueue { - #region Constants - - private static readonly byte UPDATE_COMMAND = 0x02; - - #endregion - #region Properties & Fields private readonly string _hostname; - private readonly UdpClient _udpClient; + private HttpClient _httpClient = new HttpClient(); + private UdpClient _udpClient; + private readonly Dictionary _dataBuffer = new Dictionary(); private readonly Dictionary _sequenceNumbers = new Dictionary(); + private readonly Action _sendDataAction; + #endregion #region Constructors /// /// Initializes a new instance of the class. + /// If this constructor is used UDP updates are disabled. /// /// The update trigger used by this queue. /// The hostname to connect to. - /// The port used by the web-connection. - public NodeMCUWS2812USBUpdateQueue(IDeviceUpdateTrigger updateTrigger, string hostname, int port) + public NodeMCUWS2812USBUpdateQueue(IDeviceUpdateTrigger updateTrigger, string hostname) : base(updateTrigger) { this._hostname = hostname; - _udpClient = new UdpClient(_hostname, port); - _udpClient.Connect(_hostname, port); + _sendDataAction = SendHttp; + } + + /// + /// Initializes a new instance of the class. + /// If this constructor is used UDP updates are enabled. + /// + /// The update trigger used by this queue. + /// The hostname to connect to. + /// The port used by the UDP-connection. + public NodeMCUWS2812USBUpdateQueue(IDeviceUpdateTrigger updateTrigger, string hostname, int udpPort) + : base(updateTrigger) + { + this._hostname = hostname; + + _udpClient = new UdpClient(); + EnableUdp(udpPort); + + _sendDataAction = SendUdp; } #endregion #region Methods + /// + protected override void OnStartup(object sender, CustomUpdateData customData) + { + base.OnStartup(sender, customData); + + ResetDevice(); + } + /// protected override void Update(Dictionary dataSet) { foreach (IGrouping channelData in dataSet.Select(x => (((int channel, int key))x.Key, x.Value)) .GroupBy(x => x.Item1.channel)) { - int channel = channelData.Key; - byte[] buffer = _dataBuffer[channel]; - - buffer[0] = GetSequenceNumber(channel); - buffer[1] = (byte)((channel << 4) | UPDATE_COMMAND); - int i = 2; - foreach ((byte _, byte r, byte g, byte b) in channelData.OrderBy(x => x.Item1.key) - .Select(x => x.Value.GetRGBBytes())) - { - buffer[i++] = r; - buffer[i++] = g; - buffer[i++] = b; - } - - Send(buffer); + byte[] buffer = GetBuffer(channelData); + _sendDataAction(buffer); } } + private void SendHttp(byte[] buffer) + { + string data = Convert.ToBase64String(buffer); + lock (_httpClient) _httpClient?.PostAsync(GetUrl("update"), new StringContent(data, Encoding.ASCII)).Wait(); + } + + private void SendUdp(byte[] buffer) + { + _udpClient?.Send(buffer, buffer.Length); + } + + private byte[] GetBuffer(IGrouping data) + { + int channel = data.Key; + byte[] buffer = _dataBuffer[channel]; + + buffer[0] = GetSequenceNumber(channel); + buffer[1] = (byte)channel; + int i = 2; + foreach ((byte _, byte r, byte g, byte b) in data.OrderBy(x => x.Item1.key) + .Select(x => x.Value.GetRGBBytes())) + { + buffer[i++] = r; + buffer[i++] = g; + buffer[i++] = b; + } + + return buffer; + } + internal IEnumerable<(int channel, int ledCount)> GetChannels() { - WebClient webClient = new WebClient(); - webClient.DownloadString($"http://{_hostname}/reset"); + string configString; + lock (_httpClient) configString = _httpClient.GetStringAsync(GetUrl("config")).Result; - int channelCount = int.Parse(webClient.DownloadString($"http://{_hostname}/channels")); - for (int channel = 1; channel <= channelCount; channel++) + configString = configString.Replace(" ", "") + .Replace("\r", "") + .Replace("\n", ""); + + //HACK DarthAffe 13.07.2020: Adding a JSON-Parser dependency just for this is not really worth it right now ... + MatchCollection channelMatches = Regex.Matches(configString, "\\{\"channel\":(?\\d+),\"leds\":(?\\d+)\\}"); + foreach (Match channelMatch in channelMatches) { - int ledCount = int.Parse(webClient.DownloadString($"http://{_hostname}/channel/{channel}")); - if (ledCount > 0) + int channel = int.Parse(channelMatch.Groups["channel"].Value); + int leds = int.Parse(channelMatch.Groups["leds"].Value); + if (leds > 0) { - _dataBuffer[channel] = new byte[(ledCount * 3) + 2]; + _dataBuffer[channel] = new byte[(leds * 3) + 2]; _sequenceNumbers[channel] = 0; - yield return (channel, ledCount); + yield return (channel, leds); } } } - private void Send(byte[] data) => _udpClient.Send(data, data.Length); + internal void ResetDevice() + { + lock (_httpClient) _httpClient.GetStringAsync(GetUrl("reset")).Wait(); + } + + private void EnableUdp(int port) + { + _httpClient.PostAsync(GetUrl("enableUDP"), new StringContent(port.ToString(), Encoding.UTF8, "application/json")).Result.Content.ReadAsStringAsync().Wait(); + _udpClient.Connect(_hostname, port); + } private byte GetSequenceNumber(int channel) { - byte sequenceNumber = (byte)((_sequenceNumbers[channel] + 1) % byte.MaxValue); + byte sequenceNumber = (byte)Math.Max(1, (_sequenceNumbers[channel] + 1) % byte.MaxValue); _sequenceNumbers[channel] = sequenceNumber; return sequenceNumber; } + /// + public override void Dispose() + { + lock (_httpClient) + { + base.Dispose(); + +#if NETSTANDARD + _udpClient?.Dispose(); +#endif + _udpClient = null; + + ResetDevice(); + _httpClient.Dispose(); + _httpClient = null; + } + } + + private string GetUrl(string path) => $"http://{_hostname}/{path}"; + #endregion } } diff --git a/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS281XDeviceDefinition.cs b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS281XDeviceDefinition.cs index 14d5a05..f573eab 100644 --- a/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS281XDeviceDefinition.cs +++ b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS281XDeviceDefinition.cs @@ -2,6 +2,7 @@ // ReSharper disable UnusedMember.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global +using System; using System.Collections.Generic; using RGB.NET.Core; @@ -22,9 +23,14 @@ namespace RGB.NET.Devices.WS281X.NodeMCU public string Hostname { get; } /// - /// Gets the port of the UDP connection. + /// Gets or sets the port of the UDP connection. /// - public int Port { get; } + public int Port { get; set; } = 1872; + + /// + /// Gets or sets the update-mode of the device. + /// + public NodeMCUUpdateMode UpdateMode { get; set; } /// /// Gets or sets the name used by this device. @@ -40,11 +46,11 @@ namespace RGB.NET.Devices.WS281X.NodeMCU /// Initializes a new instance of the class. /// /// The hostname to connect to. - /// The port of the UDP connection. - public NodeMCUWS281XDeviceDefinition(string hostname, int port = 1872) + /// The update mode of the device. + public NodeMCUWS281XDeviceDefinition(string hostname, NodeMCUUpdateMode updateMode = NodeMCUUpdateMode.Udp) { this.Hostname = hostname; - this.Port = port; + this.UpdateMode = updateMode; } #endregion @@ -54,7 +60,13 @@ namespace RGB.NET.Devices.WS281X.NodeMCU /// public IEnumerable CreateDevices(IDeviceUpdateTrigger updateTrigger) { - NodeMCUWS2812USBUpdateQueue queue = new NodeMCUWS2812USBUpdateQueue(updateTrigger, Hostname, Port); + NodeMCUWS2812USBUpdateQueue queue = UpdateMode switch + { + NodeMCUUpdateMode.Http => new NodeMCUWS2812USBUpdateQueue(updateTrigger, Hostname), + NodeMCUUpdateMode.Udp => new NodeMCUWS2812USBUpdateQueue(updateTrigger, Hostname, Port), + _ => throw new ArgumentOutOfRangeException() + }; + IEnumerable<(int channel, int ledCount)> channels = queue.GetChannels(); int counter = 0; foreach ((int channel, int ledCount) in channels) diff --git a/RGB.NET.Devices.WS281X/RGB.NET.Devices.WS281X.csproj b/RGB.NET.Devices.WS281X/RGB.NET.Devices.WS281X.csproj index d2ce907..c8a987d 100644 --- a/RGB.NET.Devices.WS281X/RGB.NET.Devices.WS281X.csproj +++ b/RGB.NET.Devices.WS281X/RGB.NET.Devices.WS281X.csproj @@ -64,6 +64,7 @@ + diff --git a/RGB.NET.Devices.WS281X/Sketches/RGB.NET_NodeMCU.ino b/RGB.NET.Devices.WS281X/Sketches/RGB.NET_NodeMCU.ino new file mode 100644 index 0000000..dc28c66 --- /dev/null +++ b/RGB.NET.Devices.WS281X/Sketches/RGB.NET_NodeMCU.ino @@ -0,0 +1,295 @@ +#define FASTLED_ESP8266_RAW_PIN_ORDER + +#include "FastLED.h" +#include +#include +#include +#include "base64.hpp" + +//#### CONFIGURATION #### + +// WLAN settings +const char* ssid = ""; // WLAN-network-name +const char* password = ""; // WLAN-password + +#define CHANNELS 4 // change this only if you add or remove channels in the implementation-part. To disable channels set them to 0 leds. + +// should not exceed 168 leds, since that results in the maximum paket size that is safe to transmit. Everything above could potentially be dropped. +// no more than 255 leds per channel (hard limit) +#define LEDS_CHANNEL_1 3 +#define LEDS_CHANNEL_2 0 +#define LEDS_CHANNEL_3 0 +#define LEDS_CHANNEL_4 0 + +#define PIN_CHANNEL_1 15 // D8 +#define PIN_CHANNEL_2 13 // D7 +#define PIN_CHANNEL_3 12 // D6 +#define PIN_CHANNEL_4 14 // D5 + +#define WEBSERVER_PORT 80 + +//####################### + +CRGB leds_channel_1[LEDS_CHANNEL_1]; +CRGB leds_channel_2[LEDS_CHANNEL_2]; +CRGB leds_channel_3[LEDS_CHANNEL_3]; +CRGB leds_channel_4[LEDS_CHANNEL_4]; + +ESP8266WebServer server(80); +WiFiUDP Udp; + +bool isUDPEnabled; +int udpPort; +byte incomingPacket[767]; // 255 (max leds) * 3 + 2 (header) +byte lastSequenceNumbers[CHANNELS]; + +bool checkSequenceNumber(int channel, byte currentSequenceNumber) +{ + bool isValid = (currentSequenceNumber > lastSequenceNumbers[channel]) || ((lastSequenceNumbers[channel] > 200) && (currentSequenceNumber < 50)); + if(isValid) + { + lastSequenceNumbers[channel] = currentSequenceNumber; + } + return isValid; +} + +void processUDP() +{ + int packetSize = Udp.parsePacket(); + if (packetSize) + { + // receive incoming UDP packets + byte sequenceNumber = Udp.read(); + byte channel = Udp.read(); + if(checkSequenceNumber(channel, sequenceNumber)) + { + switch(channel) + { + case 1: // set leds of channel 1 + Udp.read((uint8_t*)leds_channel_1, (LEDS_CHANNEL_1 * 3)); + FastLED.show(); + break; + + // ### channel 2 ### + case 2: // set leds of channel 2 + Udp.read((uint8_t*)leds_channel_2, (LEDS_CHANNEL_2 * 3)); + FastLED.show(); + break; + + // ### channel 3 ### + case 3: // set leds of channel 3 + Udp.read((uint8_t*)leds_channel_3, (LEDS_CHANNEL_3 * 3)); + FastLED.show(); + break; + + // ### channel 4 ### + case 4: // set leds of channel 4 + Udp.read((uint8_t*)leds_channel_4, (LEDS_CHANNEL_4 * 3)); + FastLED.show(); + break; + + // ### default ### + default: + break; + } + } + } +} + +void handleRoot() +{ + String infoSite = (String)"\ + \ + RGB.NET\ + \ + \ +

RGB.NET

\ + This device is currently running the NodeMCU WS281X RGB.NET-Sketch.
\ +
\ + Check https://github.com/DarthAffe/RGB.NET for more info and the latest version of this sketch.
\ +
\ +

Configuration:

\ + UDP:\ " + (isUDPEnabled ? ((String)"enabled (" + udpPort + ")") : "disabled") + "
\ +
\ + Channel 1
\ + Leds: " + LEDS_CHANNEL_1 + "
\ + Pin: " + PIN_CHANNEL_1 + "
\ +
\ + Channel 2
\ + Leds: " + LEDS_CHANNEL_2 + "
\ + Pin: " + PIN_CHANNEL_2 + "
\ +
\ + Channel 4
\ + Leds: " + LEDS_CHANNEL_3 + "
\ + Pin: " + PIN_CHANNEL_3 + "
\ +
\ + Channel 4
\ + Leds: " + LEDS_CHANNEL_4 + "
\ + Pin: " + PIN_CHANNEL_4 + "
\ +
\ + \ +"; + + server.send(200, "text/html", infoSite); +} + +void handleConfig() +{ + String config = (String)"{\ + \"channels\": [\ + {\ + \"channel\": 1,\ + \"leds\": " + LEDS_CHANNEL_1 + "\ + },\ + {\ + \"channel\": 2,\ + \"leds\": " + LEDS_CHANNEL_2 + "\ + },\ + {\ + \"channel\": 3,\ + \"leds\": " + LEDS_CHANNEL_3 + "\ + },\ + {\ + \"channel\": 4,\ + \"leds\": " + LEDS_CHANNEL_4 + "\ + }\ + ]\ +}"; + + server.send(200, "application/json", config); +} + +void handleEnableUDP() +{ + if(isUDPEnabled) + { + Udp.stop(); + } + + udpPort = server.arg(0).toInt(); + + Udp.begin(udpPort); + isUDPEnabled = true; + + server.send(200, "text/html", ""); +} + +void handleDisableUDP() +{ + if(isUDPEnabled) + { + Udp.stop(); + isUDPEnabled = false; + } + + server.send(200, "text/html", ""); +} + +void handleReset() +{ + for(int i = 0; i < CHANNELS; i++) + { + lastSequenceNumbers[i] = 0; + } + + for(int i = 0; i < LEDS_CHANNEL_1; i++) + { + leds_channel_1[i] = CRGB::Black; + } + + for(int i = 0; i < LEDS_CHANNEL_2; i++) + { + leds_channel_2[i] = CRGB::Black; + } + + for(int i = 0; i < LEDS_CHANNEL_3; i++) + { + leds_channel_3[i] = CRGB::Black; + } + + for(int i = 0; i < LEDS_CHANNEL_4; i++) + { + leds_channel_4[i] = CRGB::Black; + } + + FastLED.show(); + + server.send(200, "text/html", ""); +} + +void handleUpdate() +{ + unsigned int dataLength = decode_base64((unsigned char*)server.arg(0).c_str(), incomingPacket); + + byte channel = (byte)incomingPacket[1]; + switch(channel) + { + case 1: // set leds of channel 1 + memcpy((uint8_t*)leds_channel_1, &incomingPacket[2], (LEDS_CHANNEL_1 * 3)); + FastLED.show(); + break; + + // ### channel 2 ### + case 2: // set leds of channel 2 + memcpy((uint8_t*)leds_channel_2, &incomingPacket[2], (LEDS_CHANNEL_2 * 3)); + FastLED.show(); + break; + + // ### channel 3 ### + case 3: // set leds of channel 3 + memcpy((uint8_t*)leds_channel_3, &incomingPacket[2], (LEDS_CHANNEL_3 * 3)); + FastLED.show(); + break; + + // ### channel 4 ### + case 4: // set leds of channel 4 + memcpy((uint8_t*)leds_channel_4, &incomingPacket[2], (LEDS_CHANNEL_4 * 3)); + FastLED.show(); + break; + + // ### default ### + default: + break; + } + + server.send(200, "text/html", ""); +} + +void setup() +{ + if(LEDS_CHANNEL_1 > 0) { FastLED.addLeds(leds_channel_1, LEDS_CHANNEL_1); } + if(LEDS_CHANNEL_2 > 0) { FastLED.addLeds(leds_channel_2, LEDS_CHANNEL_2); } + if(LEDS_CHANNEL_3 > 0) { FastLED.addLeds(leds_channel_3, LEDS_CHANNEL_3); } + if(LEDS_CHANNEL_4 > 0) { FastLED.addLeds(leds_channel_4, LEDS_CHANNEL_4); } + + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + } + + delay(100); + + server.on("/", handleRoot); + server.on("/config", handleConfig); + server.on("/enableUDP", handleEnableUDP); + server.on("/disableUDP", handleDisableUDP); + server.on("/reset", handleReset); + server.on("/update", handleUpdate); + server.onNotFound(handleRoot); + + server.begin(); + + handleReset(); +} + +void loop() +{ + server.handleClient(); + + if(isUDPEnabled) + { + processUDP(); + } +} + \ No newline at end of file diff --git a/RGB.NET.Devices.WS281X/Sketches/RGB.NET_udp.ino b/RGB.NET.Devices.WS281X/Sketches/RGB.NET_udp.ino deleted file mode 100644 index 91f03d2..0000000 --- a/RGB.NET.Devices.WS281X/Sketches/RGB.NET_udp.ino +++ /dev/null @@ -1,210 +0,0 @@ -#define FASTLED_ESP8266_RAW_PIN_ORDER - -#include "FastLED.h" -#include -#include - -//#### CONFIGURATION #### - -// WLAN settings -const char* ssid = ""; // WLAN-network-name -const char* password = ""; // WLAN-password - -#define CHANNELS 4 // change this only if you add or remove channels in the implementation-part. To disable channels set them to 0 leds. - -// should not exceed 168 leds, since that results in the maximum paket size that is safe to transmit. Everything above could potentially be dropped. -// no more than 255 leds per channel (hard limit) -#define LEDS_CHANNEL_1 3 -#define LEDS_CHANNEL_2 0 -#define LEDS_CHANNEL_3 0 -#define LEDS_CHANNEL_4 0 - -#define PIN_CHANNEL_1 15 // D8 -#define PIN_CHANNEL_2 13 // D7 -#define PIN_CHANNEL_3 12 // D6 -#define PIN_CHANNEL_4 14 // D5 - -#define PORT 1872 // if changed needs to be configured in RGB.NET (default: 1872) - -#define WEBSERVER_PORT 80 - -//####################### - -CRGB leds_channel_1[LEDS_CHANNEL_1]; -CRGB leds_channel_2[LEDS_CHANNEL_2]; -CRGB leds_channel_3[LEDS_CHANNEL_3]; -CRGB leds_channel_4[LEDS_CHANNEL_4]; - -WiFiServer server(WEBSERVER_PORT); -WiFiUDP Udp; - -byte incomingPacket[767]; // 255 (max leds) * 3 + 2 (header) -byte lastSequenceNumbers[CHANNELS]; - -String header; - -void setup() { - if(LEDS_CHANNEL_1 > 0) { FastLED.addLeds(leds_channel_1, LEDS_CHANNEL_1); } - if(LEDS_CHANNEL_2 > 0) { FastLED.addLeds(leds_channel_2, LEDS_CHANNEL_2); } - if(LEDS_CHANNEL_3 > 0) { FastLED.addLeds(leds_channel_3, LEDS_CHANNEL_3); } - if(LEDS_CHANNEL_4 > 0) { FastLED.addLeds(leds_channel_4, LEDS_CHANNEL_4); } - - for(int i = 0; i < CHANNELS; i++) - { - lastSequenceNumbers[i] = 255; - } - - WiFi.begin(ssid, password); - while (WiFi.status() != WL_CONNECTED) - { - delay(500); - } - delay(100); - - Udp.begin(PORT); - server.begin(); -} - -bool checkSequenceNumber(int channel, byte currentSequenceNumber) -{ - bool isValid = (currentSequenceNumber > lastSequenceNumbers[channel]) || ((lastSequenceNumbers[channel] > 200) && (currentSequenceNumber < 50)); - if(isValid) - { - lastSequenceNumbers[channel] = currentSequenceNumber; - } - return isValid; -} - -void loop() { - // Web client - WiFiClient client = server.available(); - if (client) - { - String currentLine = ""; - while (client.connected()) - { - if (client.available()) - { - char c = client.read(); - header += c; - if (c == '\n') { - if (currentLine.length() == 0) { - client.println("HTTP/1.1 200 OK"); - client.println("Content-type:text/html"); - client.println("Connection: close"); - client.println(); - - if (header.indexOf("GET /reset") >= 0) - { - for(int i = 0; i < CHANNELS; i++) - { - lastSequenceNumbers[i] = 255; - } - for(int i = 0; i < LEDS_CHANNEL_1; i++) - { - leds_channel_1[i] = CRGB::Black; - } - for(int i = 0; i < LEDS_CHANNEL_2; i++) - { - leds_channel_2[i] = CRGB::Black; - } - for(int i = 0; i < LEDS_CHANNEL_3; i++) - { - leds_channel_3[i] = CRGB::Black; - } - for(int i = 0; i < LEDS_CHANNEL_4; i++) - { - leds_channel_4[i] = CRGB::Black; - } - FastLED.show(); - } - else if (header.indexOf("GET /channels") >= 0) - { - client.println(CHANNELS); - } - else if (header.indexOf("GET /channel/1") >= 0) - { - client.println(LEDS_CHANNEL_1); - } - else if (header.indexOf("GET /channel/2") >= 0) - { - client.println(LEDS_CHANNEL_2); - } - else if (header.indexOf("GET /channel/3") >= 0) - { - client.println(LEDS_CHANNEL_3); - } - else if (header.indexOf("GET /channel/4") >= 0) - { - client.println(LEDS_CHANNEL_4); - } - client.println(); - - break; - } - else - { - currentLine = ""; - } - } - else if (c != '\r') - { - currentLine += c; - } - } - } - header = ""; - client.stop(); - } - - // Color update - int packetSize = Udp.parsePacket(); - if (packetSize) - { - // receive incoming UDP packets - byte sequenceNumber = Udp.read(); - byte command = Udp.read(); - switch(command) - { - // ### channel 1 ### - case 0x12: // set leds of channel 1 - if(checkSequenceNumber(0, sequenceNumber)) - { - Udp.read(((uint8_t*)leds_channel_1), (LEDS_CHANNEL_1 * 3)); - FastLED.show(); - } - break; - - // ### channel 2 ### - case 0x22: // set leds of channel 2 - if(checkSequenceNumber(1, sequenceNumber)) - { - Udp.read(((uint8_t*)leds_channel_2), (LEDS_CHANNEL_2 * 3)); - FastLED.show(); - } - break; - - // ### channel 3 ### - case 0x32: // set leds of channel 3 - if(checkSequenceNumber(2, sequenceNumber)) - { - Udp.read(((uint8_t*)leds_channel_3), (LEDS_CHANNEL_3 * 3)); - FastLED.show(); - } - break; - - // ### channel 4 ### - case 0x42: // set leds of channel 4 - if(checkSequenceNumber(3, sequenceNumber)) - { - Udp.read(((uint8_t*)leds_channel_4), (LEDS_CHANNEL_4 * 3)); - FastLED.show(); - } - break; - - // ### default ### - default: - break; - } - } -} diff --git a/RGB.NET.Devices.WS281X/WS281XDeviceProvider.cs b/RGB.NET.Devices.WS281X/WS281XDeviceProvider.cs index 89ad1cc..3875cf4 100644 --- a/RGB.NET.Devices.WS281X/WS281XDeviceProvider.cs +++ b/RGB.NET.Devices.WS281X/WS281XDeviceProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using RGB.NET.Core; +using RGB.NET.Devices.WS281X.NodeMCU; namespace RGB.NET.Devices.WS281X { @@ -105,7 +106,11 @@ namespace RGB.NET.Devices.WS281X /// public void ResetDevices() - { } + { + foreach (IRGBDevice device in Devices) + if (device is NodeMCUWS2812USBDevice nodemcuDevice) + nodemcuDevice.UpdateQueue.ResetDevice(); + } /// public void Dispose() From 1e8c6596c55b1d5327245dcaabad8863dcbd7ec9 Mon Sep 17 00:00:00 2001 From: DarthAffe Date: Tue, 14 Jul 2020 14:28:20 +0200 Subject: [PATCH 4/4] Used port-constant for web-server creation in NodeMCU-sketch Co-authored-by: Leon Kiefer --- RGB.NET.Devices.WS281X/Sketches/RGB.NET_NodeMCU.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RGB.NET.Devices.WS281X/Sketches/RGB.NET_NodeMCU.ino b/RGB.NET.Devices.WS281X/Sketches/RGB.NET_NodeMCU.ino index dc28c66..02feab3 100644 --- a/RGB.NET.Devices.WS281X/Sketches/RGB.NET_NodeMCU.ino +++ b/RGB.NET.Devices.WS281X/Sketches/RGB.NET_NodeMCU.ino @@ -35,7 +35,7 @@ CRGB leds_channel_2[LEDS_CHANNEL_2]; CRGB leds_channel_3[LEDS_CHANNEL_3]; CRGB leds_channel_4[LEDS_CHANNEL_4]; -ESP8266WebServer server(80); +ESP8266WebServer server(WEBSERVER_PORT); WiFiUDP Udp; bool isUDPEnabled; @@ -292,4 +292,4 @@ void loop() processUDP(); } } - \ No newline at end of file +