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; 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/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..f5feffe --- /dev/null +++ b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS2812USBUpdateQueue.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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 +{ + // ReSharper disable once InconsistentNaming + /// + /// + /// Represents the update-queue performing updates for NodeMCU WS2812 devices. + /// + public class NodeMCUWS2812USBUpdateQueue : UpdateQueue + { + #region Properties & Fields + + private readonly string _hostname; + + 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. + public NodeMCUWS2812USBUpdateQueue(IDeviceUpdateTrigger updateTrigger, string hostname) + : base(updateTrigger) + { + this._hostname = hostname; + + _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)) + { + 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() + { + string configString; + lock (_httpClient) configString = _httpClient.GetStringAsync(GetUrl("config")).Result; + + 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 channel = int.Parse(channelMatch.Groups["channel"].Value); + int leds = int.Parse(channelMatch.Groups["leds"].Value); + if (leds > 0) + { + _dataBuffer[channel] = new byte[(leds * 3) + 2]; + _sequenceNumbers[channel] = 0; + yield return (channel, leds); + } + } + } + + 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)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 new file mode 100644 index 0000000..f573eab --- /dev/null +++ b/RGB.NET.Devices.WS281X/NodeMCU/NodeMCUWS281XDeviceDefinition.cs @@ -0,0 +1,83 @@ +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global + +using System; +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 or sets the port of the UDP connection. + /// + 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. + /// 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 update mode of the device. + public NodeMCUWS281XDeviceDefinition(string hostname, NodeMCUUpdateMode updateMode = NodeMCUUpdateMode.Udp) + { + this.Hostname = hostname; + this.UpdateMode = updateMode; + } + + #endregion + + #region Methods + + /// + public IEnumerable CreateDevices(IDeviceUpdateTrigger updateTrigger) + { + 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) + { + 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/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..02feab3 --- /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(WEBSERVER_PORT); +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(); + } +} + 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()