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; + } + } +}