1
0
mirror of https://github.com/DarthAffe/RGB.NET.git synced 2025-12-12 17:48:31 +00:00

Merge pull request #142 from DarthAffe/SDK/WS281X

Added device-provider for NodeMCU based WS281X-leds
This commit is contained in:
DarthAffe 2020-08-04 21:55:22 +02:00 committed by GitHub
commit 4927d76e21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 730 additions and 2 deletions

View File

@ -100,7 +100,8 @@ namespace RGB.NET.Core
_currentDataSet = null;
}
public void Dispose()
/// <inheritdoc />
public virtual void Dispose()
{
_updateTrigger.Starting -= OnStartup;
_updateTrigger.Update -= OnUpdate;

View File

@ -0,0 +1,21 @@
namespace RGB.NET.Devices.WS281X.NodeMCU
{
/// <summary>
/// Contaisn a list of possible update-modes for NodeMCU-devices.
/// </summary>
// ReSharper disable once InconsistentNaming
public enum NodeMCUUpdateMode
{
/// <summary>
/// Updates through the HTTP-REST-API.
/// Slow, but reliable.
/// </summary>
Http,
/// <summary>
/// Updates through a UDP-connection.
/// Fast, but might skip updates if the network connection is bad.
/// </summary>
Udp
}
}

View File

@ -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
/// <inheritdoc cref="AbstractRGBDevice{TDeviceInfo}" />
/// <summary>
/// Represents an NodeMCU WS2812 device.
/// </summary>
public class NodeMCUWS2812USBDevice : AbstractRGBDevice<NodeMCUWS2812USBDeviceInfo>, ILedStripe
{
#region Properties & Fields
/// <summary>
/// Gets the update queue performing updates for this device.
/// </summary>
public NodeMCUWS2812USBUpdateQueue UpdateQueue { get; }
/// <inheritdoc />
public override NodeMCUWS2812USBDeviceInfo DeviceInfo { get; }
/// <summary>
/// Gets the channel (as defined in the NodeMCU-sketch) this device is attached to.
/// </summary>
public int Channel { get; }
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="NodeMCUWS2812USBDevice"/> class.
/// </summary>
/// <param name="deviceInfo">The update trigger used by this queue.</param>
/// <param name="updateQueue">The update queue performing updates for this device.</param>
/// <param name="channel">The channel (as defined in the NodeMCU-sketch) this device is attached to.</param>
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);
}
}
/// <inheritdoc />
protected override object CreateLedCustomData(LedId ledId) => (Channel, (int)ledId - (int)LedId.LedStripe1);
/// <inheritdoc />
protected override IEnumerable<Led> GetLedsToUpdate(bool flushLeds) => (flushLeds || LedMapping.Values.Any(x => x.IsDirty)) ? LedMapping.Values : null;
/// <inheritdoc />
protected override void UpdateLeds(IEnumerable<Led> ledsToUpdate) => UpdateQueue.SetData(ledsToUpdate.Where(x => x.Color.A > 0));
/// <inheritdoc />
public override void Dispose()
{
try { UpdateQueue?.Dispose(); }
catch { /* at least we tried */ }
base.Dispose();
}
#endregion
}
}

View File

@ -0,0 +1,51 @@
using System;
using RGB.NET.Core;
namespace RGB.NET.Devices.WS281X.NodeMCU
{
// ReSharper disable once InconsistentNaming
/// <inheritdoc />
/// <summary>
/// Represents a generic information for a <see cref="T:RGB.NET.Devices.WS281X.NodeMCU.NodeMCUWS2812USBDevice" />.
/// </summary>
public class NodeMCUWS2812USBDeviceInfo : IRGBDeviceInfo
{
#region Properties & Fields
/// <inheritdoc />
public string DeviceName { get; }
/// <inheritdoc />
public RGBDeviceType DeviceType => RGBDeviceType.LedStripe;
/// <inheritdoc />
public string Manufacturer => "NodeMCU";
/// <inheritdoc />
public string Model => "WS2812 WLAN";
/// <inheritdoc />
public RGBDeviceLighting Lighting => RGBDeviceLighting.Key;
/// <inheritdoc />
public bool SupportsSyncBack => false;
/// <inheritdoc />
public Uri Image { get; set; }
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="NodeMCUWS2812USBDeviceInfo"/> class.
/// </summary>
/// <param name="name">The name of this device.</param>
public NodeMCUWS2812USBDeviceInfo(string name)
{
this.DeviceName = name;
}
#endregion
}
}

View File

@ -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
/// <inheritdoc />
/// <summary>
/// Represents the update-queue performing updates for NodeMCU WS2812 devices.
/// </summary>
public class NodeMCUWS2812USBUpdateQueue : UpdateQueue
{
#region Properties & Fields
private readonly string _hostname;
private HttpClient _httpClient = new HttpClient();
private UdpClient _udpClient;
private readonly Dictionary<int, byte[]> _dataBuffer = new Dictionary<int, byte[]>();
private readonly Dictionary<int, byte> _sequenceNumbers = new Dictionary<int, byte>();
private readonly Action<byte[]> _sendDataAction;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="NodeMCUWS2812USBUpdateQueue"/> class.
/// If this constructor is used UDP updates are disabled.
/// </summary>
/// <param name="updateTrigger">The update trigger used by this queue.</param>
/// <param name="hostname">The hostname to connect to.</param>
public NodeMCUWS2812USBUpdateQueue(IDeviceUpdateTrigger updateTrigger, string hostname)
: base(updateTrigger)
{
this._hostname = hostname;
_sendDataAction = SendHttp;
}
/// <summary>
/// Initializes a new instance of the <see cref="NodeMCUWS2812USBUpdateQueue"/> class.
/// If this constructor is used UDP updates are enabled.
/// </summary>
/// <param name="updateTrigger">The update trigger used by this queue.</param>
/// <param name="hostname">The hostname to connect to.</param>
/// <param name="udpPort">The port used by the UDP-connection.</param>
public NodeMCUWS2812USBUpdateQueue(IDeviceUpdateTrigger updateTrigger, string hostname, int udpPort)
: base(updateTrigger)
{
this._hostname = hostname;
_udpClient = new UdpClient();
EnableUdp(udpPort);
_sendDataAction = SendUdp;
}
#endregion
#region Methods
/// <inheritdoc />
protected override void OnStartup(object sender, CustomUpdateData customData)
{
base.OnStartup(sender, customData);
ResetDevice();
}
/// <inheritdoc />
protected override void Update(Dictionary<object, Color> dataSet)
{
foreach (IGrouping<int, ((int channel, int key), Color Value)> 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<int, ((int channel, int key), Color Value)> 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\":(?<channel>\\d+),\"leds\":(?<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;
}
/// <inheritdoc />
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
}
}

View File

@ -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
/// <inheritdoc />
/// <summary>
/// Represents a definition of an NodeMCU WS2812 devices.
/// </summary>
public class NodeMCUWS281XDeviceDefinition : IWS281XDeviceDefinition
{
#region Properties & Fields
/// <summary>
/// Gets the hostname to connect to.
/// </summary>
public string Hostname { get; }
/// <summary>
/// Gets or sets the port of the UDP connection.
/// </summary>
public int Port { get; set; } = 1872;
/// <summary>
/// Gets or sets the update-mode of the device.
/// </summary>
public NodeMCUUpdateMode UpdateMode { get; set; }
/// <summary>
/// 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.
/// </summary>
public string Name { get; set; }
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="NodeMCUWS281XDeviceDefinition"/> class.
/// </summary>
/// <param name="hostname">The hostname to connect to.</param>
/// <param name="updateMode">The update mode of the device.</param>
public NodeMCUWS281XDeviceDefinition(string hostname, NodeMCUUpdateMode updateMode = NodeMCUUpdateMode.Udp)
{
this.Hostname = hostname;
this.UpdateMode = updateMode;
}
#endregion
#region Methods
/// <inheritdoc />
public IEnumerable<IRGBDevice> 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
}
}

View File

@ -64,6 +64,7 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net45'">
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="Microsoft.Net.Http" Version="2.2.29" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">

View File

@ -0,0 +1,295 @@
#define FASTLED_ESP8266_RAW_PIN_ORDER
#include "FastLED.h"
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WiFiUdp.h>
#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)"<html>\
<head>\
<title>RGB.NET</title>\
</head>\
<body>\
<h1>RGB.NET</h1>\
This device is currently running the NodeMCU WS281X RGB.NET-Sketch.<br />\
<br />\
Check <a href=\"https://github.com/DarthAffe/RGB.NET\">https://github.com/DarthAffe/RGB.NET</a> for more info and the latest version of this sketch.<br />\
<br />\
<h3>Configuration:</h3>\
<b>UDP:</b>\ " + (isUDPEnabled ? ((String)"enabled (" + udpPort + ")") : "disabled") + "<br />\
<br />\
<b>Channel 1</b><br />\
Leds: " + LEDS_CHANNEL_1 + "<br />\
Pin: " + PIN_CHANNEL_1 + "<br />\
<br />\
<b>Channel 2</b><br />\
Leds: " + LEDS_CHANNEL_2 + "<br />\
Pin: " + PIN_CHANNEL_2 + "<br />\
<br />\
<b>Channel 4</b><br />\
Leds: " + LEDS_CHANNEL_3 + "<br />\
Pin: " + PIN_CHANNEL_3 + "<br />\
<br />\
<b>Channel 4</b><br />\
Leds: " + LEDS_CHANNEL_4 + "<br />\
Pin: " + PIN_CHANNEL_4 + "<br />\
<br />\
</body>\
</html>";
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<NEOPIXEL, PIN_CHANNEL_1>(leds_channel_1, LEDS_CHANNEL_1); }
if(LEDS_CHANNEL_2 > 0) { FastLED.addLeds<NEOPIXEL, PIN_CHANNEL_2>(leds_channel_2, LEDS_CHANNEL_2); }
if(LEDS_CHANNEL_3 > 0) { FastLED.addLeds<NEOPIXEL, PIN_CHANNEL_3>(leds_channel_3, LEDS_CHANNEL_3); }
if(LEDS_CHANNEL_4 > 0) { FastLED.addLeds<NEOPIXEL, PIN_CHANNEL_4>(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();
}
}

View File

@ -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
/// <inheritdoc />
public void ResetDevices()
{ }
{
foreach (IRGBDevice device in Devices)
if (device is NodeMCUWS2812USBDevice nodemcuDevice)
nodemcuDevice.UpdateQueue.ResetDevice();
}
/// <inheritdoc />
public void Dispose()