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 sealed class NodeMCUWS2812USBUpdateQueue : UpdateQueue { #region Properties & Fields private readonly string _hostname; private HttpClient _httpClient = new(); private UdpClient? _udpClient; private readonly Dictionary _dataBuffer = []; private readonly Dictionary _sequenceNumbers = []; 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 bool Update(ReadOnlySpan<(object key, Color color)> dataSet) { try { foreach (IGrouping channelData in dataSet.ToArray() .Select(x => (((int channel, int key))x.key, x.color)).GroupBy(x => x.Item1.channel)) { byte[] buffer = GetBuffer(channelData); _sendDataAction(buffer); } return true; } catch (Exception ex) { WS281XDeviceProvider.Instance.Throw(ex); } return false; } 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.identifier.key) .Select(x => x.color.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(); _udpClient?.Dispose(); _udpClient = null; ResetDevice(); _httpClient.Dispose(); } } private string GetUrl(string path) => $"http://{_hostname}/{path}"; #endregion }