diff --git a/RGB.NET.Devices.PicoPi/Enum/UpdateMode.cs b/RGB.NET.Devices.PicoPi/Enum/UpdateMode.cs new file mode 100644 index 0000000..6be450c --- /dev/null +++ b/RGB.NET.Devices.PicoPi/Enum/UpdateMode.cs @@ -0,0 +1,9 @@ +namespace RGB.NET.Devices.PicoPi.Enum +{ + public enum UpdateMode + { + Auto = 0x00, + HID = 0x01, + BULK = 0x02, + } +} diff --git a/RGB.NET.Devices.PicoPi/PicoPi/LedMappings.cs b/RGB.NET.Devices.PicoPi/PicoPi/LedMappings.cs new file mode 100644 index 0000000..dd37db9 --- /dev/null +++ b/RGB.NET.Devices.PicoPi/PicoPi/LedMappings.cs @@ -0,0 +1,23 @@ +using RGB.NET.Core; + +namespace RGB.NET.Devices.PicoPi +{ + public static class LedMappings + { + #region Properties & Fields + + public static LedMapping StripeMapping = new(); + + #endregion + + #region Constructors + + static LedMappings() + { + for (int i = 0; i < 255; i++) + StripeMapping.Add(LedId.LedStripe1 + i, i); + } + + #endregion + } +} diff --git a/RGB.NET.Devices.PicoPi/PicoPi/PicoPiBulkUpdateQueue.cs b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiBulkUpdateQueue.cs new file mode 100644 index 0000000..a92b191 --- /dev/null +++ b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiBulkUpdateQueue.cs @@ -0,0 +1,52 @@ +using System; +using RGB.NET.Core; + +namespace RGB.NET.Devices.PicoPi +{ + public class PicoPiBulkUpdateQueue : UpdateQueue + { + #region Properties & Fields + + private readonly PicoPiSDK _sdk; + private readonly int _channel; + + private readonly byte[] _dataBuffer; + + #endregion + + #region Constructors + + public PicoPiBulkUpdateQueue(IDeviceUpdateTrigger updateTrigger, PicoPiSDK sdk, int channel, int ledCount) + : base(updateTrigger) + { + this._sdk = sdk; + this._channel = channel; + + _dataBuffer = new byte[ledCount * 3]; + } + + #endregion + + #region Methods + + protected override void Update(in ReadOnlySpan<(object key, Color color)> dataSet) + { + Span buffer = _dataBuffer; + foreach ((object key, Color color) in dataSet) + { + int index = key as int? ?? -1; + if (index < 0) continue; + + (byte _, byte r, byte g, byte b) = color.GetRGBBytes(); + int offset = index * 3; + buffer[offset] = r; + buffer[offset + 1] = g; + buffer[offset + 2] = b; + } + + _sdk.SendBulkUpdate(buffer, _channel); + } + + #endregion + } +} diff --git a/RGB.NET.Devices.PicoPi/PicoPi/PicoPiHIDUpdateQueue.cs b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiHIDUpdateQueue.cs new file mode 100644 index 0000000..1ac89ed --- /dev/null +++ b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiHIDUpdateQueue.cs @@ -0,0 +1,66 @@ +using System; +using RGB.NET.Core; + +namespace RGB.NET.Devices.PicoPi +{ + public class PicoPiHIDUpdateQueue : UpdateQueue + { + #region Constants + + private const int OFFSET_MULTIPLIER = 60; + + #endregion + + #region Properties & Fields + + private readonly PicoPiSDK _sdk; + private readonly int _channel; + + private readonly byte[] _dataBuffer; + + #endregion + + #region Constructors + + public PicoPiHIDUpdateQueue(IDeviceUpdateTrigger updateTrigger, PicoPiSDK sdk, int channel, int ledCount) + : base(updateTrigger) + { + this._sdk = sdk; + this._channel = channel; + + _dataBuffer = new byte[ledCount * 3]; + } + + #endregion + + #region Methods + + protected override void Update(in ReadOnlySpan<(object key, Color color)> dataSet) + { + Span buffer = _dataBuffer; + foreach ((object key, Color color) in dataSet) + { + int index = key as int? ?? -1; + if (index < 0) continue; + + (byte _, byte r, byte g, byte b) = color.GetRGBBytes(); + int offset = index * 3; + buffer[offset] = r; + buffer[offset + 1] = g; + buffer[offset + 2] = b; + } + + int chunks = _dataBuffer.Length / OFFSET_MULTIPLIER; + if ((chunks * OFFSET_MULTIPLIER) < buffer.Length) chunks++; + for (int i = 0; i < chunks; i++) + { + int offset = i * OFFSET_MULTIPLIER; + int length = Math.Min(buffer.Length - offset, OFFSET_MULTIPLIER); + bool update = i == (chunks - 1); + _sdk.SendHidUpdate(buffer.Slice(offset, length), _channel, i, update); + } + } + + #endregion + } +} diff --git a/RGB.NET.Devices.PicoPi/PicoPi/PicoPiRGBDevice.cs b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiRGBDevice.cs new file mode 100644 index 0000000..d10e03d --- /dev/null +++ b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiRGBDevice.cs @@ -0,0 +1,36 @@ +using RGB.NET.Core; + +namespace RGB.NET.Devices.PicoPi +{ + public class PicoPiRGBDevice : AbstractRGBDevice + { + #region Properties & Fields + + private readonly LedMapping _ledMapping; + + #endregion + + #region Constructors + + public PicoPiRGBDevice(PicoPiRGBDeviceInfo deviceInfo, IUpdateQueue updateQueue, LedMapping ledMapping) + : base(deviceInfo, updateQueue) + { + this._ledMapping = ledMapping; + } + + #endregion + + #region Methods + + internal void Initialize() + { + for (int i = 0; i < DeviceInfo.LedCount; i++) + AddLed(_ledMapping[i], new Point(i * 10, 0), new Size(10, 10), i); + } + + /// + protected override object GetLedCustomData(LedId ledId) => _ledMapping.TryGetValue(ledId, out int index) ? index : -1; + + #endregion + } +} diff --git a/RGB.NET.Devices.PicoPi/PicoPi/PicoPiRGBDeviceInfo.cs b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiRGBDeviceInfo.cs new file mode 100644 index 0000000..33081e8 --- /dev/null +++ b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiRGBDeviceInfo.cs @@ -0,0 +1,42 @@ +using RGB.NET.Core; + +namespace RGB.NET.Devices.PicoPi +{ + public class PicoPiRGBDeviceInfo : IRGBDeviceInfo + { + #region Properties & Fields + + public RGBDeviceType DeviceType { get; } + public string DeviceName { get; } + public string Manufacturer => "RGB.NET"; + public string Model { get; } + public object? LayoutMetadata { get; set; } + + public int Id { get; } + public int Version { get; } + public int Channel { get; } + public int LedCount { get; } + + #endregion + + #region Constructors + + internal PicoPiRGBDeviceInfo(RGBDeviceType deviceType, string model, int id, int version, int channel, int ledCount) + { + this.DeviceType = deviceType; + this.Model = model; + this.Id = id; + this.Version = version; + this.Channel = channel; + this.LedCount = ledCount; + + DeviceName = DeviceHelper.CreateDeviceName(Manufacturer, $"{Model} {id} (Channel {channel})"); + } + + #endregion + + #region Methods + + #endregion + } +} diff --git a/RGB.NET.Devices.PicoPi/PicoPi/PicoPiSDK.cs b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiSDK.cs new file mode 100644 index 0000000..bc4a88b --- /dev/null +++ b/RGB.NET.Devices.PicoPi/PicoPi/PicoPiSDK.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using HidSharp; +using LibUsbDotNet.LibUsb; +using LibUsbDotNet.Main; + +namespace RGB.NET.Devices.PicoPi +{ + public class PicoPiSDK : IDisposable + { + #region Constants + + public const int VENDOR_ID = 0x1209; + public const int HID_BULK_CONTROLLER_PID = 0x2812; + + private const byte COMMAND_CHANNEL_COUNT = 0x01; + private const byte COMMAND_LEDCOUNTS = 0x0A; + private const byte COMMAND_PINS = 0x0B; + private const byte COMMAND_ID = 0x0E; + private const byte COMMAND_VERSION = 0x0F; + private const byte COMMAND_UPDATE = 0x01; + private const byte COMMAND_UPDATE_BULK = 0x02; + + #endregion + + #region Properties & Fields + + private readonly HidDevice _hidDevice; + private readonly HidStream _hidStream; + + private UsbContext? _usbContext; + private IUsbDevice? _bulkDevice; + private UsbEndpointWriter? _bulkWriter; + + private readonly byte[] _hidSendBuffer; + private readonly byte[] _bulkSendBuffer; + + private int _bulkTransferLength = 0; + + public bool IsBulkSupported { get; private set; } + + public int Id { get; } + public int Version { get; } + public IReadOnlyList<(int channel, int ledCount)> Channels { get; } + + #endregion + + #region Constructors + + public PicoPiSDK(HidDevice device) + { + this._hidDevice = device; + + _hidSendBuffer = new byte[_hidDevice.GetMaxOutputReportLength() - 1]; + + _hidStream = _hidDevice.Open(); + LoadBulkDevice(); + + Id = GetId(); + Version = GetVersion(); + Channels = new ReadOnlyCollection<(int channel, int ledCount)>(GetChannels().ToList()); + + _bulkSendBuffer = new byte[(Channels.Sum(c => c.ledCount + 1) * 3) + 5]; + } + + #endregion + + #region Methods + + private void LoadBulkDevice() + { + try + { + _usbContext = new UsbContext(); + // DarthAffe 24.04.2021: Not using .Find as it's not returning the device :( + IEnumerable devices = _usbContext.List().Where(d => (d.VendorId == _hidDevice.VendorID) && (d.ProductId == _hidDevice.ProductID)); + foreach (IUsbDevice device in devices) + { + try + { + device.Open(); + if (device.Info.SerialNumber == _hidDevice.GetSerialNumber()) + { + _bulkDevice = device; + break; + } + device.Dispose(); + } + catch { /**/ } + } + + if (_bulkDevice != null) + { + _bulkDevice.ClaimInterface(1); + _bulkWriter = _bulkDevice.OpenEndpointWriter(WriteEndpointID.Ep02, EndpointType.Bulk); + + IsBulkSupported = true; + } + } + catch + { + _bulkWriter = null; + try { _bulkDevice?.Dispose(); } catch { /**/ } + try { _usbContext?.Dispose(); } catch { /**/ } + _bulkDevice = null; + } + } + + private int GetId() + { + SendHID(0x00, COMMAND_ID); + return Read()[1]; + } + + private int GetVersion() + { + SendHID(0x00, COMMAND_VERSION); + return Read()[1]; + } + + private IEnumerable<(int channel, int ledCount)> GetChannels() + { + SendHID(0x00, COMMAND_CHANNEL_COUNT); + int channelCount = Read()[1]; + + for (int i = 1; i <= channelCount; i++) + { + SendHID(0x00, (byte)((i << 4) | COMMAND_LEDCOUNTS)); + int ledCount = Read()[1]; + if (ledCount > 0) + yield return (i, ledCount); + } + } + + public void SendHidUpdate(in Span data, int channel, int chunk, bool update) + { + if (data.Length == 0) return; + + Span sendBuffer = _hidSendBuffer; + sendBuffer[0] = 0x00; + sendBuffer[1] = (byte)((channel << 4) | COMMAND_UPDATE); + sendBuffer[2] = update ? (byte)1 : (byte)0; + sendBuffer[3] = (byte)chunk; + data.CopyTo(sendBuffer.Slice(4, data.Length)); + SendHID(_hidSendBuffer); + } + + public void SendBulkUpdate(in Span data, int channel) + { + if ((data.Length == 0) || !IsBulkSupported) return; + + Span sendBuffer = new Span(_bulkSendBuffer).Slice(2); + int payloadSize = data.Length; + + sendBuffer[_bulkTransferLength++] = (byte)((channel << 4) | COMMAND_UPDATE_BULK); + sendBuffer[_bulkTransferLength++] = (byte)((payloadSize >> 8) & 0xFF); + sendBuffer[_bulkTransferLength++] = (byte)(payloadSize & 0xFF); + data.CopyTo(sendBuffer.Slice(_bulkTransferLength, payloadSize)); + _bulkTransferLength += payloadSize; + } + + public void FlushBulk() + { + if (_bulkTransferLength == 0) return; + + _bulkSendBuffer[0] = (byte)((_bulkTransferLength >> 8) & 0xFF); + _bulkSendBuffer[1] = (byte)(_bulkTransferLength & 0xFF); + SendBulk(_bulkSendBuffer, _bulkTransferLength + 2); + + _bulkTransferLength = 0; + } + + private void SendHID(params byte[] data) => _hidStream.Write(data); + private void SendBulk(byte[] data, int count) => _bulkWriter!.Write(data, 0, count, 1000, out int _); + + private byte[] Read() => _hidStream.Read(); + + public void Dispose() + { + _hidStream.Dispose(); + _bulkDevice?.Dispose(); + _usbContext?.Dispose(); + } + + #endregion + } +} diff --git a/RGB.NET.Devices.PicoPi/PicoPiDeviceProvider.cs b/RGB.NET.Devices.PicoPi/PicoPiDeviceProvider.cs new file mode 100644 index 0000000..c153965 --- /dev/null +++ b/RGB.NET.Devices.PicoPi/PicoPiDeviceProvider.cs @@ -0,0 +1,122 @@ +// ReSharper disable MemberCanBePrivate.Global + +using System; +using System.Collections.Generic; +using System.Linq; +using HidSharp; +using RGB.NET.Core; +using RGB.NET.Devices.PicoPi.Enum; +using RGB.NET.HID; + +namespace RGB.NET.Devices.PicoPi +{ + /// + /// + /// Represents a device provider responsible for PicoPi-devices. + /// + // ReSharper disable once InconsistentNaming + public class PicoPiDeviceProvider : AbstractRGBDeviceProvider + { + #region Constants + + private const int AUTO_UPDATE_MODE_CHUNK_THRESHOLD = 2; + + #endregion + + #region Properties & Fields + + private static PicoPiDeviceProvider? _instance; + /// + /// Gets the singleton instance. + /// + public static PicoPiDeviceProvider Instance => _instance ?? new PicoPiDeviceProvider(); + + public static HIDLoader DeviceDefinitions { get; } = new(PicoPiSDK.VENDOR_ID) + { + { PicoPiSDK.HID_BULK_CONTROLLER_PID, RGBDeviceType.LedStripe, "WS1812B-Controller", LedMappings.StripeMapping, 0 }, + }; + + private readonly List _sdks = new(); + + public UpdateMode UpdateMode { get; set; } = UpdateMode.Auto; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// Thrown if this constructor is called even if there is already an instance of this class. + public PicoPiDeviceProvider() + { + if (_instance != null) throw new InvalidOperationException($"There can be only one instance of type {nameof(PicoPiDeviceProvider)}"); + _instance = this; + } + + #endregion + + #region Methods + + protected override void InitializeSDK() { } + + /// + protected override IEnumerable GetLoadedDevices(RGBDeviceType loadFilter) + { + DeviceDefinitions.LoadFilter = loadFilter; + + return base.GetLoadedDevices(loadFilter); + } + + /// + protected override IEnumerable LoadDevices() + { + IEnumerable<(HIDDeviceDefinition definition, HidDevice device)> devices = DeviceDefinitions.GetConnectedDevices(); + foreach ((HIDDeviceDefinition definition, HidDevice device) in devices) + { + PicoPiSDK sdk = new(device); + _sdks.Add(sdk); + IDeviceUpdateTrigger updateTrigger = GetUpdateTrigger(sdk.Id); + foreach ((int channel, int ledCount) in sdk.Channels) + { + PicoPiRGBDevice picoPiDevice = new(new PicoPiRGBDeviceInfo(definition.DeviceType, definition.Name, sdk.Id, sdk.Version, channel, ledCount), GetUpdateQueue(updateTrigger, sdk, channel, ledCount), definition.LedMapping); + picoPiDevice.Initialize(); + yield return picoPiDevice; + } + + if (sdk.IsBulkSupported) + updateTrigger.Update += (_, _) => sdk.FlushBulk(); + } + } + + private IUpdateQueue GetUpdateQueue(IDeviceUpdateTrigger updateTrigger, PicoPiSDK sdk, int channel, int ledCount) + { + switch (UpdateMode) + { + case UpdateMode.HID: + return new PicoPiHIDUpdateQueue(updateTrigger, sdk, channel, ledCount); + + case UpdateMode.BULK: + if (!sdk.IsBulkSupported) throw new NotSupportedException("Bulk updates aren't supported for this device. Make sure the firmware is built with bulk support and the libusb driver is installed."); + return new PicoPiBulkUpdateQueue(updateTrigger, sdk, channel, ledCount); + + case UpdateMode.Auto: + if (!sdk.IsBulkSupported || (sdk.Channels.Sum(c => (int)Math.Ceiling(c.ledCount / 20.0)) <= AUTO_UPDATE_MODE_CHUNK_THRESHOLD)) return new PicoPiHIDUpdateQueue(updateTrigger, sdk, channel, ledCount); + return new PicoPiBulkUpdateQueue(updateTrigger, sdk, channel, ledCount); + + default: throw new IndexOutOfRangeException($"Update mode {UpdateMode} is not supported."); + } + } + + protected override void Reset() + { + base.Reset(); + + foreach (PicoPiSDK sdk in _sdks) + sdk.Dispose(); + _sdks.Clear(); + } + + #endregion + } +} diff --git a/RGB.NET.Devices.PicoPi/RGB.NET.Devices.PicoPi.csproj b/RGB.NET.Devices.PicoPi/RGB.NET.Devices.PicoPi.csproj new file mode 100644 index 0000000..7d0040e --- /dev/null +++ b/RGB.NET.Devices.PicoPi/RGB.NET.Devices.PicoPi.csproj @@ -0,0 +1,61 @@ + + + net5.0 + latest + enable + + Darth Affe + Wyrez + en-US + en-US + RGB.NET.Devices.PicoPi + RGB.NET.Devices.PicoPi + RGB.NET.Devices.PicoPi + RGB.NET.Devices.PicoPi + RGB.NET.Devices.PicoPi + PicoPi-Device-Implementations of RGB.NET + PicoPi-Device-Implementations of RGB.NET, a C# (.NET) library for accessing various RGB-peripherals + Copyright © Darth Affe 2020 + Copyright © Darth Affe 2020 + http://lib.arge.be/icon.png + https://github.com/DarthAffe/RGB.NET + https://raw.githubusercontent.com/DarthAffe/RGB.NET/master/LICENSE + Github + https://github.com/DarthAffe/RGB.NET + True + + + + 0.0.1 + 0.0.1 + 0.0.1 + + ..\bin\ + true + True + True + + + + $(DefineConstants);TRACE;DEBUG + true + full + false + + + + pdbonly + true + $(NoWarn);CS1591;CS1572;CS1573 + $(DefineConstants);RELEASE + + + + + + + + + + + \ No newline at end of file diff --git a/RGB.NET.Devices.PicoPi/RGB.NET.Devices.PicoPi.csproj.DotSettings b/RGB.NET.Devices.PicoPi/RGB.NET.Devices.PicoPi.csproj.DotSettings new file mode 100644 index 0000000..2f92998 --- /dev/null +++ b/RGB.NET.Devices.PicoPi/RGB.NET.Devices.PicoPi.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/RGB.NET.Devices.PicoPi/index.md b/RGB.NET.Devices.PicoPi/index.md new file mode 100644 index 0000000..301905f --- /dev/null +++ b/RGB.NET.Devices.PicoPi/index.md @@ -0,0 +1 @@ +Check https://github.com/DarthAffe/RGB.NET-PicoPi for the required firmware. \ No newline at end of file diff --git a/RGB.NET.sln b/RGB.NET.sln index 3737317..f37972b 100644 --- a/RGB.NET.sln +++ b/RGB.NET.sln @@ -41,6 +41,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RGB.NET.Layout", "RGB.NET.L EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RGB.NET.HID", "RGB.NET.HID\RGB.NET.HID.csproj", "{4F4B7329-4858-4314-BA32-9DF3B1B33482}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RGB.NET.Devices.PicoPi", "RGB.NET.Devices.PicoPi\RGB.NET.Devices.PicoPi.csproj", "{7FC5C7A8-7B27-46E7-A8E8-DB80568F49C5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -115,6 +117,10 @@ Global {4F4B7329-4858-4314-BA32-9DF3B1B33482}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F4B7329-4858-4314-BA32-9DF3B1B33482}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F4B7329-4858-4314-BA32-9DF3B1B33482}.Release|Any CPU.Build.0 = Release|Any CPU + {7FC5C7A8-7B27-46E7-A8E8-DB80568F49C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FC5C7A8-7B27-46E7-A8E8-DB80568F49C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FC5C7A8-7B27-46E7-A8E8-DB80568F49C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FC5C7A8-7B27-46E7-A8E8-DB80568F49C5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -133,6 +139,7 @@ Global {A3FD5AD7-040A-47CA-A278-53493A25FF8A} = {92D7C263-D4C9-4D26-93E2-93C1F9C2CD16} {E0732B34-3F96-4DD9-AFD5-0E34B833AD6D} = {D13032C6-432E-4F43-8A32-071133C22B16} {DD46DB2D-85BE-4962-86AE-E38C9053A548} = {D13032C6-432E-4F43-8A32-071133C22B16} + {7FC5C7A8-7B27-46E7-A8E8-DB80568F49C5} = {D13032C6-432E-4F43-8A32-071133C22B16} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7F222AD4-1F9E-4AAB-9D69-D62372D4C1BA}