first commit

This commit is contained in:
BlubbFish 2026-08-03 22:34:16 +02:00
commit 308ba282fb
74 changed files with 6507 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.vs
OpenMacroBoard.SDK/bin
OpenMacroBoard.SDK/obj
StreamDeckSharp/bin
StreamDeckSharp/obj

View File

@ -0,0 +1,118 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Generic;
namespace OpenMacroBoard.SDK {
/// <summary>
/// Macro board adapter that implements a software button press effect.
/// </summary>
/// <remarks>
/// <para>This vaguely mimics the perspective of a real button being pushed
/// and provides better feedback to the user that a button push was registered.</para>
/// </remarks>
public class ButtonPressEffectAdapter : MacroBoardAdapter {
private readonly Dictionary<int, KeyBitmap> mostRecentKeyBitmaps = new();
private readonly Dictionary<int, bool> keyPressedState = new();
/// <summary>
/// Initializes a new instance of the <see cref="ButtonPressEffectAdapter"/> class.
/// </summary>
/// <param name="macroBoard">The <see cref="IMacroBoard"/> this effect should be applied to.</param>
public ButtonPressEffectAdapter(IMacroBoard macroBoard)
: this(macroBoard, null) {
}
/// <summary>
/// Initializes a new instance of the <see cref="ButtonPressEffectAdapter"/> class.
/// </summary>
/// <param name="macroBoard">The board that is wrapped with the button press effect.</param>
/// <param name="config">The configuration that should be used. If null the default configuration will be used.</param>
public ButtonPressEffectAdapter(IMacroBoard macroBoard, ButtonPressEffectConfig config) : base(macroBoard) {
this.ButtonPressed += this.ButtonPressEffectAdapter_ButtonPressed;
this.ButtonReleased += this.ButtonPressEffectAdapter_ButtonReleased;
//KeyStateChanged += SoftwareButtonFeature_KeyStateChanged;
Config = config ?? new ButtonPressEffectConfig();
}
private void ButtonPressEffectAdapter_ButtonReleased(Object sender, KeyPressEvent e) {
this.keyPressedState[e.Key] = false;
this.UpdateKeyBitmap(e.Key);
}
private void ButtonPressEffectAdapter_ButtonPressed(Object sender, KeyPressEvent e) {
this.keyPressedState[e.Key] = true;
this.UpdateKeyBitmap(e.Key);
}
/// <summary>
/// The configuration that controls the behavior of the button press effect feature.
/// </summary>
public ButtonPressEffectConfig Config {
get;
}
/// <inheritdoc/>
public override void SetButtonImage(int keyId, KeyBitmap bitmapData) {
mostRecentKeyBitmaps[keyId] = bitmapData;
UpdateKeyBitmap(keyId);
}
private void UpdateKeyBitmap(int keyId) {
var bitmap = GetBitmapForKey(keyId);
if(bitmap != null) {
base.SetButtonImage(keyId, bitmap);
}
}
private bool IsKeyPressed(int keyId) {
if(keyPressedState.TryGetValue(keyId, out var keyPressed)) {
return keyPressed;
}
return false;
}
private KeyBitmap GetBitmapForKey(int keyId) {
if(!mostRecentKeyBitmaps.TryGetValue(keyId, out var bitmap)) {
return null;
}
return IsKeyPressed(keyId) ? ResizeBitmap(bitmap) : bitmap;
}
private KeyBitmap ResizeBitmap(KeyBitmap keyBitmap) {
var bitmapDataAccess = (IKeyBitmapDataAccess)keyBitmap;
if(bitmapDataAccess.IsEmpty) {
return KeyBitmap.Black;
}
var targetWidth = (int)Math.Round(Config.Scale * keyBitmap.Width);
var targetHeight = (int)Math.Round(Config.Scale * keyBitmap.Height);
var smallerImage = bitmapDataAccess.ToImage();
smallerImage.Mutate(x => x.Resize(targetWidth, targetHeight));
var offsetLeft = (int)Math.Round((keyBitmap.Width - targetWidth) * Config.OriginX);
var offsetTop = (int)Math.Round((keyBitmap.Height - targetHeight) * Config.OriginY);
var color = Color.FromPixel(new Rgb24(
Config.BackgroundColor.R,
Config.BackgroundColor.G,
Config.BackgroundColor.B)
);
var newImage = new Image<Bgr24>(keyBitmap.Width, keyBitmap.Height);
newImage.Mutate(x => {
x.BackgroundColor(color);
x.DrawImage(smallerImage, new Point(offsetLeft, offsetTop), 1);
});
return KeyBitmap.Create.FromImageSharpImage(newImage);
}
}
}

View File

@ -0,0 +1,35 @@
using System.Diagnostics.CodeAnalysis;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Configuration for <see cref="ButtonPressEffectAdapter"/>
/// </summary>
[ExcludeFromCodeCoverage]
public class ButtonPressEffectConfig
{
/// <summary>
/// Gets or sets a factor that determines how much the images gets smaller or even bigger when pressed.
/// </summary>
/// <remarks>
/// <para>It's basically a scale factor, if you want the image to be half the size use 0.5. One means no change
/// and values larger than one make the image bigger when pressed.</para>
/// </remarks>
public double Scale { get; set; } = 0.8;
/// <summary>
/// Gets or sets the relative x coordinate of the origin.
/// </summary>
public double OriginX { get; set; } = 0.5;
/// <summary>
/// Gets or sets the relative y coordinate of the origin.
/// </summary>
public double OriginY { get; set; } = 0.5;
/// <summary>
/// The background color that is used when the button is shrunk.
/// </summary>
public OmbColor BackgroundColor { get; set; } = OmbColor.Black;
}
}

View File

@ -0,0 +1,50 @@
using System;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// A conditional disposable wrapper.
/// </summary>
/// <remarks>
/// <para>This class is used in situations where the wrapped element is either borrowed (in which case
/// it shouldn't be disposed) or owned (in which case it should be disposed) and abstracts that
/// away from the consumer. The consumer has to make sure to call dispose once they are finished
/// and this wrapped decides whether the wrapped element is in fact disposed or not.</para>
/// </remarks>
/// <typeparam name="T">Disposable type.</typeparam>
public sealed class ConditionalDisposable<T> : IDisposable
where T : IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="ConditionalDisposable{T}"/> class.
/// </summary>
/// <param name="item">The wrapped item.</param>
/// <param name="disposeItem">A flag that determines if the item will be disposed.</param>
public ConditionalDisposable(T item, bool disposeItem)
{
Item = item;
DisposeItem = disposeItem;
}
/// <summary>
/// Gets the underlying wrapped item.
/// </summary>
public T Item { get; }
/// <summary>
/// Get a value that determines whether the item will be disposed or not.
/// </summary>
public bool DisposeItem { get; }
/// <inheritdoc />
public void Dispose()
{
if (!DisposeItem)
{
return;
}
Item?.Dispose();
}
}
}

View File

@ -0,0 +1,23 @@
using System;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Is used for events that communicate connection changes.
/// </summary>
public class ConnectionEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionEventArgs"/> class.
/// </summary>
public ConnectionEventArgs(bool newConnectionState)
{
NewConnectionState = newConnectionState;
}
/// <summary>
/// The new connection state.
/// </summary>
public bool NewConnectionState { get; }
}
}

View File

@ -0,0 +1,32 @@
using System;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// An event argument that reports a connection status change for a particular device.
/// </summary>
public class DeviceConnectionChangedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="DeviceConnectionChangedEventArgs"/> class.
/// </summary>
/// <param name="deviceReference">A device reference.</param>
/// <param name="connected">The current connection state.</param>
public DeviceConnectionChangedEventArgs(IDeviceReference deviceReference, bool connected)
{
DeviceReference = deviceReference ?? throw new ArgumentNullException(nameof(deviceReference));
Connected = connected;
}
/// <summary>
/// Gets a handle to the device that changed.
/// </summary>
public IDeviceReference DeviceReference { get; }
/// <summary>
/// Gets a value that indicates the connection state change. True if the device got connected,
/// false if the device got disconnected.
/// </summary>
public bool Connected { get; }
}
}

View File

@ -0,0 +1,19 @@
using OpenMacroBoard.SDK.Internals;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// A collection of <see cref="IDeviceContext"/> released methods.
/// </summary>
public static class DeviceContext
{
/// <summary>
/// Creates a new device context (without any listeners).
/// </summary>
/// <returns>A new device context.</returns>
public static IDeviceContext Create()
{
return new DeviceContextInternal();
}
}
}

View File

@ -0,0 +1,125 @@
using OpenMacroBoard.SDK.Internals;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace OpenMacroBoard.SDK
{
/// <summary>
/// A collection of extensions for <see cref="IDeviceContext"/>.
/// </summary>
public static class DeviceContextExtensions
{
/// <summary>
/// Wait for and open the first detected <see cref="IMacroBoard"/> in this context.
/// </summary>
/// <remarks>
/// <para>
/// If there are multiple devices connected it's undefined which one you will get.
/// If you want to specify a filter to get a specific device use
/// <see cref="OpenAsync(IDeviceContext, Func{IDeviceReference, bool}, CancellationToken)"/>.
/// </para>
/// </remarks>
public static Task<IMacroBoard> OpenAsync(
this IDeviceContext context,
CancellationToken cancellationToken = default
)
{
return context.OpenAsync(_ => true, cancellationToken);
}
/// <summary>
/// Wait for and open the first matching <see cref="IMacroBoard"/> in this context.
/// </summary>
public static async Task<IMacroBoard> OpenAsync(
this IDeviceContext context,
Func<IDeviceReference, bool> selector,
CancellationToken cancellationToken = default
)
{
return (await context.GetDeviceReferenceAsync(selector, cancellationToken)).Open();
}
/// <summary>
/// Wait for and return the first matching <see cref="IDeviceReference"/>.
/// </summary>
public static async Task<IDeviceReference> GetDeviceReferenceAsync(
this IDeviceContext context,
Func<IDeviceReference, bool> selector,
CancellationToken cancellationToken = default
)
{
// check if there already is a matching device.
var devRef = context.KnownDevices.FirstOrDefault(selector);
if (devRef is not null)
{
return devRef;
}
// if not, register event handler
// and wait for a matching device to show up
var eventSync = new object();
var waitForDeviceBlocker = new TaskCompletionSource<int>();
IDeviceReference? foundDevice = null;
void ProcessReport(DeviceStateReport report)
{
lock (eventSync!)
{
if (foundDevice != null)
{
// ignore events if we've already found a device.
return;
}
if (report.Connected && selector(report.DeviceReference))
{
foundDevice = report.DeviceReference;
waitForDeviceBlocker!.TrySetResult(0);
}
}
}
IDisposable subscription = null!;
CancellationTokenRegistration registration = default;
try
{
registration = cancellationToken.Register(() => waitForDeviceBlocker.TrySetCanceled());
var deviceReportObserver = new DeviceStateObserver(ProcessReport);
subscription = context.DeviceStateReports.Subscribe(deviceReportObserver);
await waitForDeviceBlocker.Task;
}
finally
{
#if NET8_0_OR_GREATER
await registration.DisposeAsync();
#else
registration.Dispose();
#endif
subscription.Dispose();
}
#pragma warning disable S2583 // Conditionally executed code should be reachable
if (foundDevice is null)
#pragma warning restore S2583
{
// We don't document that exception because it shouldn't happen,
// and we don't expect the consumer to handle this exception type.
#pragma warning disable RCS1140 // Add exception to documentation comment.
throw new InvalidOperationException("Couldn't locate device with matching criteria. This is a bug, please file an issue.");
#pragma warning restore RCS1140
}
return foundDevice;
}
}
}

View File

@ -0,0 +1,131 @@
using OpenMacroBoard.SDK.Internals;
using System;
using System.Collections.Generic;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// A device listener base to simplify device listener implementations.
/// </summary>
public abstract class DeviceListenerBase : IObservable<DeviceStateReport>
{
private readonly object sync = new();
private readonly List<Subscription> subscriptions = new();
private readonly List<KnownDeviceInternal> knownDevices;
/// <summary>
/// Initializes a new instance of the <see cref="DeviceListenerBase"/> class.
/// </summary>
protected DeviceListenerBase()
{
knownDevices = new();
KnownDevices = knownDevices.AsReadOnly();
}
/// <summary>
/// Gets a list of the currently known (at least seen once) devices.
/// </summary>
public IReadOnlyList<IKnownDevice> KnownDevices { get; }
/// <summary>
/// Subscribes an observer that will be notified when a device state changes.
/// </summary>
/// <returns>Returns a disposable subscription.</returns>
public IDisposable Subscribe(IObserver<DeviceStateReport> observer)
{
lock (sync)
{
// send currently known values
foreach (var device in knownDevices)
{
observer.OnNext(new DeviceStateReport(device.DeviceReference, device.Connected, true));
}
// setup subscription for updates
var sub = new Subscription(this, observer);
subscriptions.Add(sub);
return sub;
}
}
/// <summary>
/// Updates a device state. If the state has changed compared to the previous state all subscribed
/// observers will be notified.
/// </summary>
/// <param name="deviceReference">A referenced device which has changed.</param>
/// <param name="connected">The current connection state of the device.</param>
protected void Update(IDeviceReference deviceReference, bool connected)
{
lock (sync)
{
var foundIndex = knownDevices.FindIndex(d => d.DeviceReference.Equals(deviceReference));
var isNew = foundIndex < 0;
if (isNew)
{
knownDevices.Add(new KnownDeviceInternal(deviceReference, connected));
foundIndex = knownDevices.Count - 1;
}
var knownDevice = knownDevices[foundIndex];
knownDevice.Connected = connected;
foreach (var subscription in subscriptions)
{
subscription.SendUpdates();
}
}
}
private sealed class Subscription : IDisposable
{
private readonly DeviceListenerBase parent;
private readonly IObserver<DeviceStateReport> observer;
/// <summary>
/// Contains the state the subscriber knows about.
/// This is used to calculate new updates.
/// </summary>
private readonly List<bool> subscriberState = new();
public Subscription(DeviceListenerBase parent, IObserver<DeviceStateReport> observer)
{
this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
this.observer = observer ?? throw new ArgumentNullException(nameof(observer));
}
public void SendUpdates()
{
// send updates for existing devices
for (int i = 0; i < subscriberState.Count; i++)
{
var device = parent.knownDevices[i];
if (device.Connected != subscriberState[i])
{
// report new connection state
observer.OnNext(new DeviceStateReport(device.DeviceReference, device.Connected, false));
subscriberState[i] = device.Connected;
}
}
// add and send updates for new (to this subscriber) devices.
for (int i = subscriberState.Count; i < parent.knownDevices.Count; i++)
{
var device = parent.knownDevices[i];
subscriberState.Add(device.Connected);
observer.OnNext(new DeviceStateReport(device.DeviceReference, device.Connected, true));
}
}
public void Dispose()
{
lock (parent.sync)
{
parent.subscriptions.Remove(this);
}
}
}
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// A device state report.
/// </summary>
public class DeviceStateReport
{
/// <summary>
/// Initializes a new instance of the <see cref="DeviceStateReport"/> class.
/// </summary>
/// <param name="deviceReference">The device.</param>
/// <param name="connected">The connection state.</param>
/// <param name="newDevice">Info about if the device is new or not.</param>
public DeviceStateReport(IDeviceReference deviceReference, bool connected, bool newDevice)
{
DeviceReference = deviceReference ?? throw new ArgumentNullException(nameof(deviceReference));
Connected = connected;
NewDevice = newDevice;
}
/// <summary>
/// Gets the device reference.
/// </summary>
public IDeviceReference DeviceReference { get; }
/// <summary>
/// Gets the connection state.
/// </summary>
public bool Connected { get; }
/// <summary>
/// Gets the info if this device is new or not.
/// </summary>
public bool NewDevice { get; }
}
}

View File

@ -0,0 +1,54 @@
using System.Collections.Generic;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// A <see cref="IMacroBoard"/> adapter that replays brightness and key bitmaps if a device is disconnected.
/// </summary>
public class DisconnectReplayAdapter : MacroBoardAdapter
{
private readonly Dictionary<int, KeyBitmap> mostRecentKeyBitmaps = new();
private byte? mostRecentBrightness = null;
/// <summary>
/// Initializes a new instance of the <see cref="DisconnectReplayAdapter"/> class.
/// </summary>
public DisconnectReplayAdapter(IMacroBoard macroBoard)
: base(macroBoard)
{
ConnectionStateChanged += ReplayEventsForConnectionStateChange;
}
/// <inheritdoc/>
public override void SetBrightness(byte percent)
{
mostRecentBrightness = percent;
base.SetBrightness(percent);
}
/// <inheritdoc/>
public override void SetButtonImage(int keyId, KeyBitmap bitmapData)
{
mostRecentKeyBitmaps[keyId] = bitmapData;
base.SetButtonImage(keyId, bitmapData);
}
private void ReplayEventsForConnectionStateChange(object sender, ConnectionEventArgs e)
{
if (e.NewConnectionState)
{
// devices connected again: replay last known values
if (mostRecentBrightness is not null)
{
base.SetBrightness(mostRecentBrightness.Value);
}
foreach (var bmp in mostRecentKeyBitmaps)
{
base.SetButtonImage(bmp.Key, bmp.Value);
}
}
}
}
}

View File

@ -0,0 +1,123 @@
using OpenMacroBoard.SDK.Internals;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Extension method to generate fullscreen images on <see cref="IMacroBoard"/>s.
/// </summary>
public static class DrawFullScreenExtensions
{
/// <summary>
/// Draw a given image as fullscreen (spanning over all keys)
/// </summary>
/// <param name="board">The board the image should be drawn to.</param>
/// <param name="image">The image that should be drawn.</param>
/// <param name="resizeMode">The resize mode that should be used to fit the image.</param>
/// <exception cref="ArgumentNullException">The provided board or bitmap is null.</exception>
public static void DrawFullScreenBitmap
(
this IMacroBoard board,
Image image,
ResizeMode resizeMode = ResizeMode.BoxPad
)
{
if (board is null)
{
throw new ArgumentNullException(nameof(board));
}
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
byte[] imgData = null;
using (var ctx = ResizeToFullStreamDeckImage(image, board.Keys.Area.Size, resizeMode))
{
imgData = ctx.Item.ToBgr24PixelArray();
}
for (var i = 0; i < board.Keys.Count; i++)
{
var img = GetKeyImageFromFull(board.Keys[i], imgData, board.Keys.Area.Size);
board.SetButtonImage(i, img);
}
}
private static ConditionalDisposable<Image<Bgr24>> ResizeToFullStreamDeckImage
(
Image image,
OmbSize newSize,
ResizeMode resizeMode
)
{
return ConstrainedContext.For(
image,
x =>
{
if (x is not Image<Bgr24> bgr24)
{
return null;
}
if (x.Width != newSize.Width || x.Height != newSize.Height)
{
return null;
}
return bgr24;
},
_ =>
{
var scaled = new Image<Bgr24>(image.Width, image.Height);
var resizeOptions = new ResizeOptions()
{
Mode = resizeMode,
Size = new(newSize.Width, newSize.Height),
Sampler = KnownResamplers.Welch,
};
scaled.Mutate(x =>
{
x.DrawImage(image, 1);
x.Resize(resizeOptions);
});
return scaled;
}
);
}
private static KeyBitmap GetKeyImageFromFull
(
OmbRectangle keyPos,
byte[] fullImageData,
OmbSize fullImageSize
)
{
var keyImgData = new byte[keyPos.Width * keyPos.Height * 3];
var stride = 3 * fullImageSize.Width;
for (var y = 0; y < keyPos.Height; y++)
{
for (var x = 0; x < keyPos.Width; x++)
{
var p = (keyPos.Top + y) * stride + (keyPos.Left + x) * 3;
var kPos = (y * keyPos.Width + x) * 3;
keyImgData[kPos + 0] = fullImageData[p + 0];
keyImgData[kPos + 1] = fullImageData[p + 1];
keyImgData[kPos + 2] = fullImageData[p + 2];
}
}
return KeyBitmap.Create.FromBgr24Array(keyPos.Width, keyPos.Height, keyImgData);
}
}
}

View File

@ -0,0 +1,749 @@
namespace OpenMacroBoard.SDK
{
/// <content>
/// Contains static named color values.
/// <see href="https://www.w3.org/TR/css-color-3/"/>
/// </content>
public readonly partial struct OmbColor
{
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F0F8FF.
/// </summary>
public static readonly OmbColor AliceBlue = FromRgb(240, 248, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FAEBD7.
/// </summary>
public static readonly OmbColor AntiqueWhite = FromRgb(250, 235, 215);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FFFF.
/// </summary>
public static readonly OmbColor Aqua = FromRgb(0, 255, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #7FFFD4.
/// </summary>
public static readonly OmbColor Aquamarine = FromRgb(127, 255, 212);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F0FFFF.
/// </summary>
public static readonly OmbColor Azure = FromRgb(240, 255, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F5F5DC.
/// </summary>
public static readonly OmbColor Beige = FromRgb(245, 245, 220);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFE4C4.
/// </summary>
public static readonly OmbColor Bisque = FromRgb(255, 228, 196);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #000000.
/// </summary>
public static readonly OmbColor Black = FromRgb(0, 0, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFEBCD.
/// </summary>
public static readonly OmbColor BlanchedAlmond = FromRgb(255, 235, 205);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #0000FF.
/// </summary>
public static readonly OmbColor Blue = FromRgb(0, 0, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8A2BE2.
/// </summary>
public static readonly OmbColor BlueViolet = FromRgb(138, 43, 226);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #A52A2A.
/// </summary>
public static readonly OmbColor Brown = FromRgb(165, 42, 42);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DEB887.
/// </summary>
public static readonly OmbColor BurlyWood = FromRgb(222, 184, 135);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #5F9EA0.
/// </summary>
public static readonly OmbColor CadetBlue = FromRgb(95, 158, 160);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #7FFF00.
/// </summary>
public static readonly OmbColor Chartreuse = FromRgb(127, 255, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D2691E.
/// </summary>
public static readonly OmbColor Chocolate = FromRgb(210, 105, 30);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF7F50.
/// </summary>
public static readonly OmbColor Coral = FromRgb(255, 127, 80);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #6495ED.
/// </summary>
public static readonly OmbColor CornflowerBlue = FromRgb(100, 149, 237);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFF8DC.
/// </summary>
public static readonly OmbColor Cornsilk = FromRgb(255, 248, 220);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DC143C.
/// </summary>
public static readonly OmbColor Crimson = FromRgb(220, 20, 60);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FFFF.
/// </summary>
public static readonly OmbColor Cyan = Aqua;
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00008B.
/// </summary>
public static readonly OmbColor DarkBlue = FromRgb(0, 0, 139);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #008B8B.
/// </summary>
public static readonly OmbColor DarkCyan = FromRgb(0, 139, 139);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #B8860B.
/// </summary>
public static readonly OmbColor DarkGoldenrod = FromRgb(184, 134, 11);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #A9A9A9.
/// </summary>
public static readonly OmbColor DarkGray = FromRgb(169, 169, 169);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #006400.
/// </summary>
public static readonly OmbColor DarkGreen = FromRgb(0, 100, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #A9A9A9.
/// </summary>
public static readonly OmbColor DarkGrey = DarkGray;
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #BDB76B.
/// </summary>
public static readonly OmbColor DarkKhaki = FromRgb(189, 183, 107);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8B008B.
/// </summary>
public static readonly OmbColor DarkMagenta = FromRgb(139, 0, 139);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #556B2F.
/// </summary>
public static readonly OmbColor DarkOliveGreen = FromRgb(85, 107, 47);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF8C00.
/// </summary>
public static readonly OmbColor DarkOrange = FromRgb(255, 140, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #9932CC.
/// </summary>
public static readonly OmbColor DarkOrchid = FromRgb(153, 50, 204);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8B0000.
/// </summary>
public static readonly OmbColor DarkRed = FromRgb(139, 0, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #E9967A.
/// </summary>
public static readonly OmbColor DarkSalmon = FromRgb(233, 150, 122);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8FBC8F.
/// </summary>
public static readonly OmbColor DarkSeaGreen = FromRgb(143, 188, 143);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #483D8B.
/// </summary>
public static readonly OmbColor DarkSlateBlue = FromRgb(72, 61, 139);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #2F4F4F.
/// </summary>
public static readonly OmbColor DarkSlateGray = FromRgb(47, 79, 79);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #2F4F4F.
/// </summary>
public static readonly OmbColor DarkSlateGrey = DarkSlateGray;
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00CED1.
/// </summary>
public static readonly OmbColor DarkTurquoise = FromRgb(0, 206, 209);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #9400D3.
/// </summary>
public static readonly OmbColor DarkViolet = FromRgb(148, 0, 211);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF1493.
/// </summary>
public static readonly OmbColor DeepPink = FromRgb(255, 20, 147);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00BFFF.
/// </summary>
public static readonly OmbColor DeepSkyBlue = FromRgb(0, 191, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #696969.
/// </summary>
public static readonly OmbColor DimGray = FromRgb(105, 105, 105);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #696969.
/// </summary>
public static readonly OmbColor DimGrey = DimGray;
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #1E90FF.
/// </summary>
public static readonly OmbColor DodgerBlue = FromRgb(30, 144, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #B22222.
/// </summary>
public static readonly OmbColor Firebrick = FromRgb(178, 34, 34);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFAF0.
/// </summary>
public static readonly OmbColor FloralWhite = FromRgb(255, 250, 240);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #228B22.
/// </summary>
public static readonly OmbColor ForestGreen = FromRgb(34, 139, 34);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF00FF.
/// </summary>
public static readonly OmbColor Fuchsia = FromRgb(255, 0, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DCDCDC.
/// </summary>
public static readonly OmbColor Gainsboro = FromRgb(220, 220, 220);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F8F8FF.
/// </summary>
public static readonly OmbColor GhostWhite = FromRgb(248, 248, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFD700.
/// </summary>
public static readonly OmbColor Gold = FromRgb(255, 215, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DAA520.
/// </summary>
public static readonly OmbColor Goldenrod = FromRgb(218, 165, 32);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #808080.
/// </summary>
public static readonly OmbColor Gray = FromRgb(128, 128, 128);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #008000.
/// </summary>
public static readonly OmbColor Green = FromRgb(0, 128, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #ADFF2F.
/// </summary>
public static readonly OmbColor GreenYellow = FromRgb(173, 255, 47);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #808080.
/// </summary>
public static readonly OmbColor Grey = Gray;
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F0FFF0.
/// </summary>
public static readonly OmbColor Honeydew = FromRgb(240, 255, 240);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF69B4.
/// </summary>
public static readonly OmbColor HotPink = FromRgb(255, 105, 180);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #CD5C5C.
/// </summary>
public static readonly OmbColor IndianRed = FromRgb(205, 92, 92);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #4B0082.
/// </summary>
public static readonly OmbColor Indigo = FromRgb(75, 0, 130);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFFF0.
/// </summary>
public static readonly OmbColor Ivory = FromRgb(255, 255, 240);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F0E68C.
/// </summary>
public static readonly OmbColor Khaki = FromRgb(240, 230, 140);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #E6E6FA.
/// </summary>
public static readonly OmbColor Lavender = FromRgb(230, 230, 250);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFF0F5.
/// </summary>
public static readonly OmbColor LavenderBlush = FromRgb(255, 240, 245);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #7CFC00.
/// </summary>
public static readonly OmbColor LawnGreen = FromRgb(124, 252, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFACD.
/// </summary>
public static readonly OmbColor LemonChiffon = FromRgb(255, 250, 205);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #ADD8E6.
/// </summary>
public static readonly OmbColor LightBlue = FromRgb(173, 216, 230);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F08080.
/// </summary>
public static readonly OmbColor LightCoral = FromRgb(240, 128, 128);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #E0FFFF.
/// </summary>
public static readonly OmbColor LightCyan = FromRgb(224, 255, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FAFAD2.
/// </summary>
public static readonly OmbColor LightGoldenrodYellow = FromRgb(250, 250, 210);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D3D3D3.
/// </summary>
public static readonly OmbColor LightGray = FromRgb(211, 211, 211);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #90EE90.
/// </summary>
public static readonly OmbColor LightGreen = FromRgb(144, 238, 144);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D3D3D3.
/// </summary>
public static readonly OmbColor LightGrey = LightGray;
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFB6C1.
/// </summary>
public static readonly OmbColor LightPink = FromRgb(255, 182, 193);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFA07A.
/// </summary>
public static readonly OmbColor LightSalmon = FromRgb(255, 160, 122);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #20B2AA.
/// </summary>
public static readonly OmbColor LightSeaGreen = FromRgb(32, 178, 170);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #87CEFA.
/// </summary>
public static readonly OmbColor LightSkyBlue = FromRgb(135, 206, 250);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #778899.
/// </summary>
public static readonly OmbColor LightSlateGray = FromRgb(119, 136, 153);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #778899.
/// </summary>
public static readonly OmbColor LightSlateGrey = LightSlateGray;
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #B0C4DE.
/// </summary>
public static readonly OmbColor LightSteelBlue = FromRgb(176, 196, 222);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFFE0.
/// </summary>
public static readonly OmbColor LightYellow = FromRgb(255, 255, 224);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FF00.
/// </summary>
public static readonly OmbColor Lime = FromRgb(0, 255, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #32CD32.
/// </summary>
public static readonly OmbColor LimeGreen = FromRgb(50, 205, 50);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FAF0E6.
/// </summary>
public static readonly OmbColor Linen = FromRgb(250, 240, 230);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF00FF.
/// </summary>
public static readonly OmbColor Magenta = Fuchsia;
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #800000.
/// </summary>
public static readonly OmbColor Maroon = FromRgb(128, 0, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #66CDAA.
/// </summary>
public static readonly OmbColor MediumAquamarine = FromRgb(102, 205, 170);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #0000CD.
/// </summary>
public static readonly OmbColor MediumBlue = FromRgb(0, 0, 205);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #BA55D3.
/// </summary>
public static readonly OmbColor MediumOrchid = FromRgb(186, 85, 211);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #9370DB.
/// </summary>
public static readonly OmbColor MediumPurple = FromRgb(147, 112, 219);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #3CB371.
/// </summary>
public static readonly OmbColor MediumSeaGreen = FromRgb(60, 179, 113);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #7B68EE.
/// </summary>
public static readonly OmbColor MediumSlateBlue = FromRgb(123, 104, 238);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FA9A.
/// </summary>
public static readonly OmbColor MediumSpringGreen = FromRgb(0, 250, 154);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #48D1CC.
/// </summary>
public static readonly OmbColor MediumTurquoise = FromRgb(72, 209, 204);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #C71585.
/// </summary>
public static readonly OmbColor MediumVioletRed = FromRgb(199, 21, 133);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #191970.
/// </summary>
public static readonly OmbColor MidnightBlue = FromRgb(25, 25, 112);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F5FFFA.
/// </summary>
public static readonly OmbColor MintCream = FromRgb(245, 255, 250);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFE4E1.
/// </summary>
public static readonly OmbColor MistyRose = FromRgb(255, 228, 225);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFE4B5.
/// </summary>
public static readonly OmbColor Moccasin = FromRgb(255, 228, 181);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFDEAD.
/// </summary>
public static readonly OmbColor NavajoWhite = FromRgb(255, 222, 173);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #000080.
/// </summary>
public static readonly OmbColor Navy = FromRgb(0, 0, 128);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FDF5E6.
/// </summary>
public static readonly OmbColor OldLace = FromRgb(253, 245, 230);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #808000.
/// </summary>
public static readonly OmbColor Olive = FromRgb(128, 128, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #6B8E23.
/// </summary>
public static readonly OmbColor OliveDrab = FromRgb(107, 142, 35);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFA500.
/// </summary>
public static readonly OmbColor Orange = FromRgb(255, 165, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF4500.
/// </summary>
public static readonly OmbColor OrangeRed = FromRgb(255, 69, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DA70D6.
/// </summary>
public static readonly OmbColor Orchid = FromRgb(218, 112, 214);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #EEE8AA.
/// </summary>
public static readonly OmbColor PaleGoldenrod = FromRgb(238, 232, 170);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #98FB98.
/// </summary>
public static readonly OmbColor PaleGreen = FromRgb(152, 251, 152);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #AFEEEE.
/// </summary>
public static readonly OmbColor PaleTurquoise = FromRgb(175, 238, 238);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DB7093.
/// </summary>
public static readonly OmbColor PaleVioletRed = FromRgb(219, 112, 147);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFEFD5.
/// </summary>
public static readonly OmbColor PapayaWhip = FromRgb(255, 239, 213);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFDAB9.
/// </summary>
public static readonly OmbColor PeachPuff = FromRgb(255, 218, 185);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #CD853F.
/// </summary>
public static readonly OmbColor Peru = FromRgb(205, 133, 63);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFC0CB.
/// </summary>
public static readonly OmbColor Pink = FromRgb(255, 192, 203);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #DDA0DD.
/// </summary>
public static readonly OmbColor Plum = FromRgb(221, 160, 221);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #B0E0E6.
/// </summary>
public static readonly OmbColor PowderBlue = FromRgb(176, 224, 230);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #800080.
/// </summary>
public static readonly OmbColor Purple = FromRgb(128, 0, 128);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #663399.
/// </summary>
public static readonly OmbColor RebeccaPurple = FromRgb(102, 51, 153);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF0000.
/// </summary>
public static readonly OmbColor Red = FromRgb(255, 0, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #BC8F8F.
/// </summary>
public static readonly OmbColor RosyBrown = FromRgb(188, 143, 143);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #4169E1.
/// </summary>
public static readonly OmbColor RoyalBlue = FromRgb(65, 105, 225);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #8B4513.
/// </summary>
public static readonly OmbColor SaddleBrown = FromRgb(139, 69, 19);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FA8072.
/// </summary>
public static readonly OmbColor Salmon = FromRgb(250, 128, 114);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F4A460.
/// </summary>
public static readonly OmbColor SandyBrown = FromRgb(244, 164, 96);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #2E8B57.
/// </summary>
public static readonly OmbColor SeaGreen = FromRgb(46, 139, 87);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFF5EE.
/// </summary>
public static readonly OmbColor SeaShell = FromRgb(255, 245, 238);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #A0522D.
/// </summary>
public static readonly OmbColor Sienna = FromRgb(160, 82, 45);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #C0C0C0.
/// </summary>
public static readonly OmbColor Silver = FromRgb(192, 192, 192);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #87CEEB.
/// </summary>
public static readonly OmbColor SkyBlue = FromRgb(135, 206, 235);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #6A5ACD.
/// </summary>
public static readonly OmbColor SlateBlue = FromRgb(106, 90, 205);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #708090.
/// </summary>
public static readonly OmbColor SlateGray = FromRgb(112, 128, 144);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #708090.
/// </summary>
public static readonly OmbColor SlateGrey = SlateGray;
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFAFA.
/// </summary>
public static readonly OmbColor Snow = FromRgb(255, 250, 250);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00FF7F.
/// </summary>
public static readonly OmbColor SpringGreen = FromRgb(0, 255, 127);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #4682B4.
/// </summary>
public static readonly OmbColor SteelBlue = FromRgb(70, 130, 180);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D2B48C.
/// </summary>
public static readonly OmbColor Tan = FromRgb(210, 180, 140);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #008080.
/// </summary>
public static readonly OmbColor Teal = FromRgb(0, 128, 128);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #D8BFD8.
/// </summary>
public static readonly OmbColor Thistle = FromRgb(216, 191, 216);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FF6347.
/// </summary>
public static readonly OmbColor Tomato = FromRgb(255, 99, 71);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #40E0D0.
/// </summary>
public static readonly OmbColor Turquoise = FromRgb(64, 224, 208);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #EE82EE.
/// </summary>
public static readonly OmbColor Violet = FromRgb(238, 130, 238);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F5DEB3.
/// </summary>
public static readonly OmbColor Wheat = FromRgb(245, 222, 179);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFFFF.
/// </summary>
public static readonly OmbColor White = FromRgb(255, 255, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #F5F5F5.
/// </summary>
public static readonly OmbColor WhiteSmoke = FromRgb(245, 245, 245);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFF00.
/// </summary>
public static readonly OmbColor Yellow = FromRgb(255, 255, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #9ACD32.
/// </summary>
public static readonly OmbColor YellowGreen = FromRgb(154, 205, 50);
}
}

View File

@ -0,0 +1,99 @@
using System;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Represents a color value.
/// </summary>
public readonly partial struct OmbColor : IEquatable<OmbColor>
{
private OmbColor(byte r, byte g, byte b)
{
R = r;
G = g;
B = b;
}
/// <summary>
/// The red part of the color.
/// </summary>
public byte R { get; }
/// <summary>
/// The green part of the color.
/// </summary>
public byte G { get; }
/// <summary>
/// The blue part of the color.
/// </summary>
public byte B { get; }
/// <summary>
/// Checks whether two <see cref="OmbColor"/> structures are equal.
/// </summary>
/// <param name="left">The left hand <see cref="OmbColor"/> operand.</param>
/// <param name="right">The right hand <see cref="OmbColor"/> operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is equal to the <paramref name="right"/> parameter;
/// otherwise, false.
/// </returns>
public static bool operator ==(OmbColor left, OmbColor right)
{
return left.Equals(right);
}
/// <summary>
/// Checks whether two <see cref="OmbColor"/> structures are equal.
/// </summary>
/// <param name="left">The left hand <see cref="OmbColor"/> operand.</param>
/// <param name="right">The right hand <see cref="OmbColor"/> operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is not equal to the <paramref name="right"/> parameter;
/// otherwise, false.
/// </returns>
public static bool operator !=(OmbColor left, OmbColor right)
{
return !left.Equals(right);
}
/// <summary>
/// Creates a <see cref="OmbColor"/> from RGB bytes.
/// </summary>
/// <param name="r">The red component (0-255).</param>
/// <param name="g">The green component (0-255).</param>
/// <param name="b">The blue component (0-255).</param>
/// <returns>The <see cref="OmbColor"/>.</returns>
public static OmbColor FromRgb(byte r, byte g, byte b)
{
return new OmbColor(r, g, b);
}
/// <inheritdoc />
public override string ToString()
{
return $"#{R:X2}{G:X2}{B:X2}";
}
/// <inheritdoc />
public bool Equals(OmbColor other)
{
return
R == other.R &&
G == other.G &&
B == other.B;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return obj is OmbColor other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(R, G, B);
}
}
}

View File

@ -0,0 +1,139 @@
using System;
using System.Runtime.CompilerServices;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Represents an ordered pair of integer x- and y-coordinates that defines a point in
/// a two-dimensional plane.
/// </summary>
public readonly struct OmbPoint : IEquatable<OmbPoint>
{
/// <summary>
/// Represents a <see cref="OmbPoint"/> that has X and Y values set to zero.
/// </summary>
public static readonly OmbPoint Empty = default;
/// <summary>
/// Initializes a new instance of the <see cref="OmbPoint"/> struct.
/// </summary>
/// <param name="x">The horizontal position of the point.</param>
/// <param name="y">The vertical position of the point.</param>
public OmbPoint(int x, int y)
: this()
{
X = x;
Y = y;
}
/// <summary>
/// Initializes a new instance of the <see cref="OmbPoint"/> struct from the given <see cref="OmbSize"/>.
/// </summary>
/// <param name="size">The size.</param>
public OmbPoint(OmbSize size)
{
X = size.Width;
Y = size.Height;
}
/// <summary>
/// Gets or sets the x-coordinate of this <see cref="OmbPoint"/>.
/// </summary>
public int X { get; }
/// <summary>
/// Gets or sets the y-coordinate of this <see cref="OmbPoint"/>.
/// </summary>
public int Y { get; }
/// <summary>
/// Gets a value indicating whether this <see cref="OmbPoint"/> is empty.
/// </summary>
public bool IsEmpty => Equals(Empty);
/// <summary>
/// Creates a <see cref="OmbSize"/> with the coordinates of the specified <see cref="OmbPoint"/>.
/// </summary>
/// <param name="point">The point.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator OmbSize(OmbPoint point)
{
return new OmbSize(point.X, point.Y);
}
/// <summary>
/// Divides <see cref="OmbPoint"/> by a <see cref="int"/> producing <see cref="OmbPoint"/>.
/// </summary>
/// <param name="left">Dividend of type <see cref="OmbPoint"/>.</param>
/// <param name="right">Divisor of type <see cref="int"/>.</param>
/// <returns>Result of type <see cref="OmbPoint"/>.</returns>
public static OmbPoint operator /(OmbPoint left, int right)
{
return new OmbPoint(left.X / right, left.Y / right);
}
/// <summary>
/// Compares two <see cref="OmbPoint"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="OmbPoint"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="OmbPoint"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(OmbPoint left, OmbPoint right)
{
return left.Equals(right);
}
/// <summary>
/// Compares two <see cref="OmbPoint"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="OmbPoint"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="OmbPoint"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(OmbPoint left, OmbPoint right)
{
return !left.Equals(right);
}
/// <summary>
/// Deconstructs this point into two integers.
/// </summary>
/// <param name="x">The out value for X.</param>
/// <param name="y">The out value for Y.</param>
public void Deconstruct(out int x, out int y)
{
x = X;
y = Y;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(X, Y);
}
/// <inheritdoc/>
public override string ToString()
{
return $"Point [ X={X}, Y={Y} ]";
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
return obj is OmbPoint other && Equals(other);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(OmbPoint other)
{
return X.Equals(other.X) && Y.Equals(other.Y);
}
}
}

View File

@ -0,0 +1,199 @@
using System;
using System.Runtime.CompilerServices;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Stores a set of four integers that represent the location and size of a rectangle.
/// </summary>
public readonly struct OmbRectangle : IEquatable<OmbRectangle>
{
/// <summary>
/// Represents a <see cref="OmbRectangle"/> that has X, Y, Width, and Height values set to zero.
/// </summary>
public static readonly OmbRectangle Empty = default;
/// <summary>
/// Initializes a new instance of the <see cref="OmbRectangle"/> struct.
/// </summary>
/// <param name="x">The horizontal position of the rectangle.</param>
/// <param name="y">The vertical position of the rectangle.</param>
/// <param name="width">The width of the rectangle.</param>
/// <param name="height">The height of the rectangle.</param>
public OmbRectangle(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
/// <summary>
/// Initializes a new instance of the <see cref="OmbRectangle"/> struct.
/// </summary>
/// <param name="point">
/// The <see cref="OmbPoint"/> which specifies the rectangles point in a two-dimensional plane.
/// </param>
/// <param name="size">
/// The <see cref="OmbSize"/> which specifies the rectangles height and width.
/// </param>
public OmbRectangle(OmbPoint point, OmbSize size)
{
X = point.X;
Y = point.Y;
Width = size.Width;
Height = size.Height;
}
/// <summary>
/// Gets or sets the x-coordinate of this <see cref="OmbRectangle"/>.
/// </summary>
public int X { get; }
/// <summary>
/// Gets or sets the y-coordinate of this <see cref="OmbRectangle"/>.
/// </summary>
public int Y { get; }
/// <summary>
/// Gets or sets the width of this <see cref="OmbRectangle"/>.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets or sets the height of this <see cref="OmbRectangle"/>.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this <see cref="OmbRectangle"/>.
/// </summary>
public OmbPoint Location
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(X, Y);
}
/// <summary>
/// Gets or sets the size of this <see cref="OmbRectangle"/>.
/// </summary>
public OmbSize Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(Width, Height);
}
/// <summary>
/// Gets a value indicating whether this <see cref="OmbRectangle"/> is empty.
/// </summary>
public bool IsEmpty => Equals(Empty);
/// <summary>
/// Gets the y-coordinate of the top edge of this <see cref="OmbRectangle"/>.
/// </summary>
public int Top => Y;
/// <summary>
/// Gets the x-coordinate of the right edge of this <see cref="OmbRectangle"/>.
/// </summary>
public int Right
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => unchecked(X + Width);
}
/// <summary>
/// Gets the y-coordinate of the bottom edge of this <see cref="OmbRectangle"/>.
/// </summary>
public int Bottom
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => unchecked(Y + Height);
}
/// <summary>
/// Gets the x-coordinate of the left edge of this <see cref="OmbRectangle"/>.
/// </summary>
public int Left => X;
/// <summary>
/// Compares two <see cref="OmbRectangle"/> objects for equality.
/// </summary>
/// <param name="left">The <see cref="OmbRectangle"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="OmbRectangle"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(OmbRectangle left, OmbRectangle right)
{
return left.Equals(right);
}
/// <summary>
/// Compares two <see cref="OmbRectangle"/> objects for inequality.
/// </summary>
/// <param name="left">The <see cref="OmbRectangle"/> on the left side of the operand.</param>
/// <param name="right">The <see cref="OmbRectangle"/> on the right side of the operand.</param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(OmbRectangle left, OmbRectangle right)
{
return !left.Equals(right);
}
/// <summary>
/// Creates a rectangle from left, top, right, bottom.
/// </summary>
public static OmbRectangle FromLTRB(int left, int top, int right, int bottom)
{
return new OmbRectangle(left, top, unchecked(right - left), unchecked(bottom - top));
}
/// <summary>
/// Deconstructs this rectangle into four integers.
/// </summary>
/// <param name="x">The out value for X.</param>
/// <param name="y">The out value for Y.</param>
/// <param name="width">The out value for the width.</param>
/// <param name="height">The out value for the height.</param>
public void Deconstruct(out int x, out int y, out int width, out int height)
{
x = X;
y = Y;
width = Width;
height = Height;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(OmbRectangle other)
{
return
X.Equals(other.X) &&
Y.Equals(other.Y) &&
Width.Equals(other.Width) &&
Height.Equals(other.Height);
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return obj is OmbRectangle other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(X, Y, Width, Height);
}
/// <inheritdoc/>
public override string ToString()
{
return $"Rectangle [ X={X}, Y={Y}, Width={Width}, Height={Height} ]";
}
}
}

View File

@ -0,0 +1,156 @@
using System;
using System.Runtime.CompilerServices;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Stores an ordered pair of integers, which specify a height and width.
/// </summary>
public readonly struct OmbSize : IEquatable<OmbSize>
{
/// <summary>
/// Represents a <see cref="OmbSize"/> that has Width and Height values set to zero.
/// </summary>
public static readonly OmbSize Empty = default;
/// <summary>
/// Initializes a new instance of the <see cref="OmbSize"/> struct.
/// </summary>
/// <param name="value">The width and height of the size.</param>
public OmbSize(int value)
: this()
{
Width = value;
Height = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="OmbSize"/> struct.
/// </summary>
/// <param name="width">The width of the size.</param>
/// <param name="height">The height of the size.</param>
public OmbSize(int width, int height)
{
Width = width;
Height = height;
}
/// <summary>
/// Initializes a new instance of the <see cref="OmbSize"/> struct.
/// </summary>
/// <param name="size">The size.</param>
public OmbSize(OmbSize size)
: this()
{
Width = size.Width;
Height = size.Height;
}
/// <summary>
/// Initializes a new instance of the <see cref="OmbSize"/> struct from the given <see cref="OmbPoint"/>.
/// </summary>
/// <param name="point">The point.</param>
public OmbSize(OmbPoint point)
{
Width = point.X;
Height = point.Y;
}
/// <summary>
/// Gets or sets the width of this <see cref="OmbSize"/>.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets or sets the height of this <see cref="OmbSize"/>.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets a value indicating whether this <see cref="OmbSize"/> is empty.
/// </summary>
public bool IsEmpty => Equals(Empty);
/// <summary>
/// Converts the given <see cref="OmbSize"/> into a <see cref="OmbPoint"/>.
/// </summary>
/// <param name="size">The size.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator OmbPoint(OmbSize size)
{
return new OmbPoint(size.Width, size.Height);
}
/// <summary>
/// Compares two <see cref="OmbSize"/> objects for equality.
/// </summary>
/// <param name="left">
/// The <see cref="OmbSize"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="OmbSize"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(OmbSize left, OmbSize right)
{
return left.Equals(right);
}
/// <summary>
/// Compares two <see cref="OmbSize"/> objects for inequality.
/// </summary>
/// <param name="left">
/// The <see cref="OmbSize"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="OmbSize"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(OmbSize left, OmbSize right)
{
return !left.Equals(right);
}
/// <summary>
/// Deconstructs this size into two integers.
/// </summary>
/// <param name="width">The out value for the width.</param>
/// <param name="height">The out value for the height.</param>
public void Deconstruct(out int width, out int height)
{
width = Width;
height = Height;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(Width, Height);
}
/// <inheritdoc/>
public override string ToString()
{
return $"Size [ Width={Width}, Height={Height} ]";
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
return obj is OmbSize other && Equals(other);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(OmbSize other)
{
return Width.Equals(other.Width) && Height.Equals(other.Height);
}
}
}

View File

@ -0,0 +1,5 @@
To make the API a bit more agnostic regarding the internal image processing library
we provide our graphic primitives (like Size, Rectangle etc.)
Those types are intentionally very simple and don't provide a lot of functionallity.
Some things are inspired on other libraries primitives like ImageSharp, SkiaSharp or System.Drawing

View File

@ -0,0 +1,96 @@
using OpenMacroBoard.SDK.Helper;
using System;
using System.Collections;
using System.Collections.Generic;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Represents a grid-like keyboard layout for macro boards.
/// </summary>
public class GridKeyLayout : IKeyLayout
{
/// <summary>
/// Initializes a new instance of the <see cref="GridKeyLayout"/> class.
/// </summary>
/// <param name="countX">Number of keys in the x-coordinate (horizontal)</param>
/// <param name="countY">Number of keys in the y-coordinate (vertical)</param>
/// <param name="keySize">Square key size (pixels)</param>
/// <param name="gapSize">Distance between keys (pixels)</param>
public GridKeyLayout(int countX, int countY, int keySize, int gapSize)
{
#pragma warning disable SA1503, IDE0011 // Braces should not be omitted
if (countX <= 0) throw new ArgumentOutOfRangeException(nameof(countX));
if (countY <= 0) throw new ArgumentOutOfRangeException(nameof(countY));
if (keySize <= 0) throw new ArgumentOutOfRangeException(nameof(keySize));
if (gapSize <= 0) throw new ArgumentOutOfRangeException(nameof(gapSize));
#pragma warning restore SA1503, IDE0011
CountX = countX;
CountY = countY;
KeySize = keySize;
GapSize = gapSize;
Count = countX * countY;
Area = this.GetFullArea();
}
/// <summary>
/// Gets the number of keys on this layout.
/// </summary>
public int Count { get; }
/// <inheritdoc />
public int KeySize { get; }
/// <inheritdoc />
public int GapSize { get; }
/// <inheritdoc />
public OmbRectangle Area { get; }
/// <inheritdoc />
public int CountX { get; }
/// <inheritdoc />
public int CountY { get; }
/// <summary>
/// Gets the dimensions of the key with a given <paramref name="keyIndex"/>.
/// </summary>
/// <param name="keyIndex">The index of the key.</param>
/// <returns>The dimensions of the requested key.</returns>
/// <exception cref="ArgumentOutOfRangeException">Is thrown if the <paramref name="keyIndex"/> is out of range.</exception>
public OmbRectangle this[int keyIndex]
{
get
{
if (keyIndex < 0 || keyIndex >= Count)
{
throw new ArgumentOutOfRangeException(nameof(keyIndex));
}
// split id into x and y component
var y = keyIndex / CountX;
var x = keyIndex % CountX;
var fullSize = KeySize + GapSize;
return new OmbRectangle(fullSize * x, fullSize * y, KeySize, KeySize);
}
}
/// <inheritdoc />
public IEnumerator<OmbRectangle> GetEnumerator()
{
for (int i = 0; i < Count; i++)
{
yield return this[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Represents a context that tracks devices and their connection state using device listeners.
/// </summary>
/// <remarks>
/// <para>This is basically the entry point to opening a <see cref="IMacroBoard"/>. Once constructed
/// you can add device listeners for various providers and the context will collect all devices
/// (even from different providers) in one place.</para>
/// </remarks>
public interface IDeviceContext : IDisposable
{
/// <summary>
/// Gets the observable that reports all device state changes, for example like
/// new devices or connection state changes.
/// </summary>
/// <remarks>
/// <para>Once subscribed, reports are generated to reflect the current state of known devices and
/// their connection state. This does not mean that all original reports are replayed, but only
/// the most recent events, that are needed to rebuild the currently known state for all known devices.</para>
/// </remarks>
IObservable<DeviceStateReport> DeviceStateReports { get; }
/// <summary>
/// A list of known devices.
/// </summary>
/// <remarks>
/// <para>The order of the list is consistent and new devices are always added at the end.</para>
/// </remarks>
IReadOnlyList<IKnownDevice> KnownDevices { get; }
/// <summary>
/// Registers a new device listener for that context.
/// </summary>
/// <remarks>
/// <para>Registered listeners can't be unsubscribed individually.
/// All listeners will unsubscribed when the context is disposed.</para>
/// </remarks>
/// <returns>Returns the original instance to allow for fluent API calls.</returns>
IDeviceContext AddListener(IObservable<DeviceStateReport> deviceListener);
/// <summary>
/// Registers a new device listener for that context.
/// </summary>
/// <remarks>
/// <para>
/// Registered listeners can't be unsubscribed individually.
/// All listeners will unsubscribed when the context is disposed.
/// </para>
/// <para>
/// When <paramref name="disposeWithContext"/> is set to true and <paramref name="deviceListener"/> is
/// <see cref="IDisposable"/> it will be disposed when the context is disposed.
/// </para>
/// </remarks>
/// <returns>Returns the original instance to allow for fluent API calls.</returns>
IDeviceContext AddListener(IObservable<DeviceStateReport> deviceListener, bool disposeWithContext);
/// <summary>
/// Registers a new device listener for that context.
/// </summary>
/// <typeparam name="TListener">A <see cref="IObservable{DeviceStateReport}"/> with a parameterless constructor.</typeparam>
/// <remarks>
/// <para>Registered listeners can't be unsubscribed individually.
/// All listeners will unsubscribed when the context is disposed.</para>
/// </remarks>
/// <returns>Returns the original instance to allow for fluent API calls.</returns>
IDeviceContext AddListener<TListener>()
where TListener : IObservable<DeviceStateReport>, new();
}
}

View File

@ -0,0 +1,29 @@
namespace OpenMacroBoard.SDK
{
/// <summary>
/// A handle that can be used to open an <see cref="IMacroBoard"/> instance.
/// </summary>
public interface IDeviceReference
{
/// <summary>
/// A user friendly display name.
/// </summary>
/// <remarks>
/// <para>The device name is not part of the equality for a device reference handle. This means, that
/// there can be two <see cref="IDeviceReference"/>s with different names which are still
/// considered to be equal, because they refer to the same device.</para>
/// </remarks>
string DeviceName { get; set; }
/// <summary>
/// Gets the key layout for the referenced device.
/// </summary>
IKeyLayout Keys { get; }
/// <summary>
/// Connects to a macro board and returns an <see cref="IMacroBoard"/> instance
/// which can be used to interact with the board.
/// </summary>
IMacroBoard Open();
}
}

View File

@ -0,0 +1,30 @@
using System;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// An interface that allows you to access the underlying data of <see cref="KeyBitmap"/>s.
/// </summary>
public interface IKeyBitmapDataAccess
{
/// <summary>
/// Gets the width of the bitmap.
/// </summary>
int Width { get; }
/// <summary>
/// Gets the height of the bitmap.
/// </summary>
int Height { get; }
/// <summary>
/// Gets a value indicating whether the underlying byte array is null.
/// </summary>
bool IsEmpty { get; }
/// <summary>
/// Gets the underlying image data in unaligned Bgr24 format (stride = width * 3).
/// </summary>
ReadOnlySpan<byte> GetData();
}
}

View File

@ -0,0 +1,14 @@
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Interface for factory extensions
/// </summary>
/// <remarks>
/// <para>This interface is intentionally empty
/// It is used to implement factory extensions
/// <see cref="KeyBitmap.Create"/></para>
/// </remarks>
public interface IKeyBitmapFactory
{
}
}

View File

@ -0,0 +1,61 @@
using System.Collections.Generic;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Describes the key layout of an <see cref="IMacroBoard"/>.
/// </summary>
public interface IKeyLayout : IReadOnlyList<OmbRectangle>
{
/// <summary>
/// Gets the image size of the keys that are supported.
/// </summary>
/// <remarks>
/// <para>
/// In the rare case that the underlying board doesn't have square keys
/// the value will be the size that most keys use, an average or a guess.
/// If you need more accurate sizes you have the enumerate the key rectangles of
/// this collection.
/// </para>
/// </remarks>
int KeySize { get; }
/// <summary>
/// Gets the smallest rectangle area that fits all keys.
/// </summary>
OmbRectangle Area { get; }
/// <summary>
/// Gets the number of keys in the horizontal direction.
/// </summary>
/// <remarks>
/// <para>In the rare case that the underlying board doesn't have a rectangular key layout
/// this value might be an estimate or even wrong. It's not even guaranteed that
/// <see cref="CountX"/> times <see cref="CountY"/> will be equal to Count
/// but all implementations should make sure that the product is at least
/// not greater than Count and <see cref="CountX"/> is at least 1.</para>
/// </remarks>
int CountX { get; }
/// <summary>
/// Gets the number of keys in the vertical direction.
/// </summary>
/// <remarks>
/// <para>In the rare case that the underlying board doesn't have a rectangular key layout
/// this value might be an estimate or even wrong. It's not even guaranteed that
/// <see cref="CountX"/> times <see cref="CountY"/> will be equal to Count
/// but all implementations should make sure that the product is at least
/// not greater than Count and <see cref="CountY"/> is at least 1.</para>
/// </remarks>
int CountY { get; }
/// <summary>
/// Gets the gap between the keys.
/// </summary>
/// <remarks>
/// <para>In the rare case that the underlying board doesn't have
/// a rectangular key layout this value might be an estimate, made up or wrong.</para>
/// </remarks>
int GapSize { get; }
}
}

View File

@ -0,0 +1,15 @@
namespace OpenMacroBoard.SDK
{
/// <summary>
/// A <see cref="IDeviceReference"/> managed by a <see cref="IDeviceContext"/>
/// with additional meta data (like the connected state).
/// </summary>
public interface IKnownDevice : IDeviceReference
{
/// <summary>
/// Gets a value that indicates the current connection state.
/// True if the device is currently connected, false if the device is disconnected.
/// </summary>
bool Connected { get; }
}
}

View File

@ -0,0 +1,140 @@
using System;
namespace OpenMacroBoard.SDK {
/// <summary>
/// An interface that allows you to interact with (LCD) macro boards
/// </summary>
public interface IMacroBoard : IDisposable {
// <summary>
// Is raised when a key is pressed
// </summary>
//event EventHandler<KeyEventArgs> KeyStateChanged;
/// <summary>
/// Is raised when the MarcoBoard is being disconnected or connected
/// </summary>
event EventHandler<ConnectionEventArgs> ConnectionStateChanged;
/// <summary>
/// Is raised when a key is pressed
/// </summary>
event EventHandler<KeyPressEvent> ButtonPressed;
/// <summary>
/// Is raised when a key is released
/// </summary>
event EventHandler<KeyPressEvent> ButtonReleased;
/// <summary>
/// Is raised when the Touchbar is tapped
/// </summary>
event EventHandler<TouchTipEvent> TouchbarTouched;
/// <summary>
/// Is raised when the Touchbar is pressed (long tap)
/// </summary>
event EventHandler<TouchTipEvent> TouchbarPressed;
/// <summary>
/// Is raised when the finger is fliped over the Touchbar
/// </summary>
event EventHandler<TouchFlipEvent> TouchFlipEvent;
/// <summary>
/// Is raised when a Rotary Encoder is pressed
/// </summary>
event EventHandler<KnobPressEvent> EncoderPressed;
/// <summary>
/// Is raised when a Rotary Encoder is released
/// </summary>
event EventHandler<KnobPressEvent> EncoderReleased;
/// <summary>
/// Is raised when a Rotary Encoder is turned
/// </summary>
event EventHandler<KnobTurnEvent> EncoderTurn;
/// <summary>
/// Informations about the keys and their position
/// </summary>
IKeyLayout Keys {
get;
}
/// <summary>
/// Gets a value indicating whether the MarcoBoard is connected.
/// </summary>
Boolean IsConnected {
get;
}
/// <summary>
/// Sets the brightness for this <see cref="IMacroBoard"/>
/// </summary>
/// <param name="percent">Brightness in percent (0 - 100)</param>
/// <remarks>
/// <para>
/// The brightness on the device is controlled with PWM (https://en.wikipedia.org/wiki/Pulse-width_modulation).
/// This results in a non-linear correlation between set percentage and perceived brightness.
/// </para>
/// <para>
/// In a nutshell: changing from 10 - 30 results in a bigger change than 80 - 100 (barely visible change)
/// This effect should be compensated outside this library
/// </para>
/// </remarks>
void SetBrightness(Byte percent);
/// <summary>
/// Uploads an image for a specific button.
/// </summary>
/// <param name="keyId">Specifies which key the image will be applied on</param>
/// <param name="bitmapData">Bitmap. The key will be painted black if this value is null.</param>
void SetButtonImage(Int32 keyId, KeyBitmap bitmapData);
/// <summary>
/// Uploads an image for the complete LCD.
/// </summary>
/// <param name="bitmapData">Bitmap. The key will be painted black if this value is null.</param>
void SetFullScreenImage(KeyBitmap bitmapData);
/// <summary>
/// Uploads a full image for the touchscreen window strip.
/// </summary>
/// <param name="bitmapData">Bitmap. The key will be painted black if this value is null.</param>
void SetWindowImage(KeyBitmap bitmapData);
/// <summary>
/// Uploads an image into a rectangular region of the touchscreen window.
/// </summary>
/// <param name="x_pos">X-coordinate</param>
/// <param name="y_pos">Y-coordinate</param>
/// <param name="width">Image width</param>
/// <param name="heigt">Image height</param>
/// <param name="bitmapData">Bitmap. The key will be painted black if this value is null.</param>
void SetPartialWindowImage(Int32 x_pos, Int32 y_pos, KeyBitmap bitmapData);
/// <summary>
/// Shows the standby logo (full-screen)
/// </summary>
void ShowLogo();
/// <summary>
/// Gets the firmware version.
/// </summary>
/// <returns>
/// Returns the firmware version
/// or <see cref="String.Empty"/> if the device doesn't have a firmware.
/// </returns>
String GetFirmwareVersion();
/// <summary>
/// Gets the serial number.
/// </summary>
/// <returns>
/// Returns the serial number
/// or <see cref="String.Empty"/> if the device doesn't have a serial number.
/// </returns>
String GetSerialNumber();
}
}

View File

@ -0,0 +1,88 @@
using OpenMacroBoard.SDK.Internals;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
namespace OpenMacroBoard.SDK {
/// <summary>
/// A collection of extensions for interop with ImageSharp.
/// </summary>
public static class ImageSharpExtensions {
/// <summary>
/// Converts an Bgr24 image to a byte array containing raw pixel data.
/// </summary>
public static byte[] ToBgr24PixelArray(this Image<Bgr24> image) {
using var ctx = image.WithBgr24();
var data = new byte[image.Width * image.Height * 3];
ctx.Item.CopyPixelDataTo(data);
return data;
}
/// <summary>
/// Copies an <see cref="Image"/> as Bgr24 into a given span.
/// </summary>
public static void ToBgr24PixelArray(this Image image, Span<byte> targetPixelData) {
using var ctx = image.WithBgr24();
ctx.Item.CopyPixelDataTo(targetPixelData);
}
/// <summary>
/// Converts an <see cref="Image"/> to a byte array containing raw pixel data.
/// </summary>
public static byte[] ToBgr24PixelArray(this Image image) {
using var ctx = image.WithBgr24();
return ctx.Item.ToBgr24PixelArray();
}
/// <summary>
/// Clones a given image into a <see cref="Image{Bgr24}"/>
/// with correct alpha blending and a given background color.
/// </summary>
public static Image<Bgr24> CloneAlphaBlendedBgr24(this Image image, OmbColor backgroundColor) {
var clonedBgr24 = new Image<Bgr24>(image.Width, image.Height);
var color = Color.FromPixel(new Rgb24(
backgroundColor.R,
backgroundColor.G,
backgroundColor.B
));
clonedBgr24.Mutate(x => {
x.BackgroundColor(color);
x.DrawImage(image, 1);
});
return clonedBgr24;
}
/// <summary>
/// Clones a given image into a <see cref="Image{Bgr24}"/>
/// with correct alpha blending and black background.
/// </summary>
public static Image<Bgr24> CloneAlphaBlendedBgr24(this Image image) {
var clonedBgr24 = new Image<Bgr24>(image.Width, image.Height);
clonedBgr24.Mutate(x => x.DrawImage(image, 1));
return clonedBgr24;
}
/// <summary>
/// Creates a context with an <see cref="Image{Bgr24}"/>.
/// </summary>
/// <remarks>
/// <para>
/// If the image already is an <see cref="Image{Bgr24}"/> this image will be wrapped inside the
/// <see cref="ConditionalDisposable{T}"/>. If the image is a different pixel format it will be cloned
/// and transformed into an <see cref="Image{Bgr24}"/>.
/// </para>
/// <para>
/// Calling <see cref="IDisposable.Dispose"/> on the returned value will never dispose the original
/// <paramref name="image"/> but only the implicitly cloned value if any.
/// </para>
/// </remarks>
public static ConditionalDisposable<Image<Bgr24>> WithBgr24(this Image image) {
return ConstrainedContext.For(image, x => x as Image<Bgr24>, x => x.CloneAlphaBlendedBgr24());
}
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace OpenMacroBoard.SDK.Internals
{
internal static class ConstrainedContext
{
/// <summary>
/// Creates a context which might depend on the provided item or a clone of that item.
/// </summary>
/// <remarks>
/// <para>This is useful in situation where you only want to do an expensive operation
/// for <see cref="IDisposable"/> types when needed. The <see cref="ConditionalDisposable{T}"/>
/// makes sure that elements that depend on the parent (borrow, no copy) will not be disposed
/// but cloned (owned copies) will be disposed.</para>
/// </remarks>
/// <typeparam name="TInput">Input type.</typeparam>
/// <typeparam name="TOutput">Output type.</typeparam>
public static ConditionalDisposable<TOutput> For<TInput, TOutput>(
TInput item,
Func<TInput, TOutput> borrow,
Func<TInput, TOutput> ownedCopy
)
where TInput : class, IDisposable
where TOutput : class, IDisposable
{
var borrowedResult = borrow(item);
if (borrowedResult is not null)
{
return new ConditionalDisposable<TOutput>(borrowedResult, false);
}
var ownedResult = ownedCopy(item);
return new ConditionalDisposable<TOutput>(ownedResult, true);
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
namespace OpenMacroBoard.SDK.Internals
{
internal sealed class DeviceContextInternal : IDeviceContext
{
private readonly MergedDeviceListener mergedDeviceListener = new();
private readonly List<IDisposable> disposeWithContext = new();
private readonly List<KnownDeviceInternal> knownDevices = new();
public DeviceContextInternal()
{
KnownDevices = knownDevices.AsReadOnly();
DeviceStateReports = mergedDeviceListener;
KnownDevices = mergedDeviceListener.KnownDevices;
}
public IReadOnlyList<IKnownDevice> KnownDevices { get; }
public IObservable<DeviceStateReport> DeviceStateReports { get; }
public void Dispose()
{
foreach (var disposable in disposeWithContext)
{
disposable.Dispose();
}
}
public IDeviceContext AddListener(IObservable<DeviceStateReport> deviceListener)
{
return AddListener(deviceListener, true);
}
public IDeviceContext AddListener(IObservable<DeviceStateReport> deviceListener, bool disposeWithContext)
{
var subscription = deviceListener.Subscribe(mergedDeviceListener);
this.disposeWithContext.Add(subscription);
if (disposeWithContext && deviceListener is IDisposable disposableListener)
{
this.disposeWithContext.Add(disposableListener);
}
return this;
}
public IDeviceContext AddListener<TListener>()
where TListener : IObservable<DeviceStateReport>, new()
{
var listener = new TListener();
return AddListener(listener, true);
}
}
}

View File

@ -0,0 +1,34 @@
using System;
#nullable enable
namespace OpenMacroBoard.SDK.Internals
{
/// <summary>
/// Create an observer from an action.
/// </summary>
internal class DeviceStateObserver : IObserver<DeviceStateReport>
{
private readonly Action<DeviceStateReport> eventHandler;
public DeviceStateObserver(Action<DeviceStateReport> eventHandler)
{
this.eventHandler = eventHandler ?? throw new ArgumentNullException(nameof(eventHandler));
}
public void OnCompleted()
{
// intentionally left empty.
}
public void OnError(Exception error)
{
// intentionally left empty.
}
public void OnNext(DeviceStateReport value)
{
eventHandler(value);
}
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace OpenMacroBoard.SDK.Internals
{
internal sealed class KnownDeviceInternal : IKnownDevice
{
public KnownDeviceInternal(IDeviceReference deviceReference, bool connected)
{
DeviceReference = deviceReference ?? throw new ArgumentNullException(nameof(deviceReference));
Connected = connected;
}
public bool Connected { get; set; }
public IDeviceReference DeviceReference { get; }
public IKeyLayout Keys => DeviceReference.Keys;
public string DeviceName
{
get => DeviceReference.DeviceName;
set => DeviceReference.DeviceName = value;
}
public override bool Equals(object obj)
{
return DeviceReference.Equals(obj);
}
public override int GetHashCode()
{
return DeviceReference.GetHashCode();
}
public IMacroBoard Open()
{
return DeviceReference.Open();
}
}
}

View File

@ -0,0 +1,26 @@
using System;
namespace OpenMacroBoard.SDK.Internals
{
internal sealed class MergedDeviceListener :
DeviceListenerBase,
IObserver<DeviceStateReport>
{
public void OnCompleted()
{
// Intentionally empty.
// When a device listener completes (which it shouldn't), we don't care.
}
public void OnError(Exception error)
{
// Intentionally empty.
// When a device listener reports an error (which it shouldn't), we don't care.
}
public void OnNext(DeviceStateReport value)
{
Update(value.DeviceReference, value.Connected);
}
}
}

View File

@ -0,0 +1,139 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace OpenMacroBoard.SDK {
/// <summary>
/// Represents a bitmap that can be used as key images
/// </summary>
public sealed class KeyBitmap : IEquatable<KeyBitmap>, IKeyBitmapDataAccess {
/// <summary>
/// Byte order is B-G-R, and pixels are stored left-to-right and top-to-bottom
/// </summary>
private readonly Byte[] rawBitmapData;
private Int32? cachedHashCode = null;
internal KeyBitmap(Int32 width, Int32 height, Byte[] bitmapData) {
this.Width = width;
this.Height = height;
this.rawBitmapData = bitmapData ?? throw new ArgumentNullException(nameof(bitmapData));
}
/// <summary>
/// This property can be used to create new KeyBitmaps
/// </summary>
/// <remarks>
/// <para>This property just serves as an anchor point for extension methods
/// to create new <see cref="KeyBitmap"/> objects</para>
/// </remarks>
public static IKeyBitmapFactory Create {
get;
}
/// <summary>
/// Solid black bitmap
/// </summary>
/// <remarks>
/// <para>If you need a black bitmap (for example to clear keys) use this property for better performance (in theory ^^)</para>
/// </remarks>
public static KeyBitmap Black { get; } = new(1, 1, []);
/// <summary>
/// Gets the width of the bitmap.
/// </summary>
public Int32 Width {
get;
}
/// <summary>
/// Gets the height of the bitmap.
/// </summary>
public Int32 Height {
get;
}
Boolean IKeyBitmapDataAccess.IsEmpty => this.rawBitmapData.Length == 0;
/// <summary>
/// The == operator
/// </summary>
public static Boolean operator ==(KeyBitmap a, KeyBitmap b) => Equals(a, b);
/// <summary>
/// The != operator
/// </summary>
public static Boolean operator !=(KeyBitmap a, KeyBitmap b) => !Equals(a, b);
/// <summary>
/// Compares the content of two given <see cref="KeyBitmap"/>s
/// </summary>
/// <param name="a">KeyBitmap a</param>
/// <param name="b">KeyBitmap b</param>
/// <returns>Returns true of the <see cref="KeyBitmap"/>s are equal and false otherwise.</returns>
public static Boolean Equals(KeyBitmap a, KeyBitmap b) =>
ReferenceEquals(a, b) || (
a is not null &&
b is not null &&
a.Width == b.Width &&
a.Height == b.Height && (
ReferenceEquals(a.rawBitmapData, b.rawBitmapData) || (
a.rawBitmapData is not null &&
b.rawBitmapData is not null &&
a.rawBitmapData.SequenceEqual(b.rawBitmapData)
)
)
);
/// <summary>
/// Compares the content of this <see cref="KeyBitmap"/> to another KeyBitmap
/// </summary>
/// <param name="other">The other <see cref="KeyBitmap"/></param>
/// <returns>True if both bitmaps are equals and false otherwise.</returns>
public Boolean Equals(KeyBitmap other) => Equals(this, other);
/// <summary>
/// Compares the content of this <see cref="KeyBitmap"/> to another object
/// </summary>
/// <param name="obj">The other object</param>
/// <returns>Return true if the other object is a <see cref="KeyBitmap"/> and equal to this one. Returns false otherwise.</returns>
public override Boolean Equals(Object obj) => Equals(this, obj as KeyBitmap);
/// <summary>
/// Get the hash code for this object.
/// </summary>
/// <returns>The hash code</returns>
public override Int32 GetHashCode() => this.cachedHashCode ??= this.CalculateObjectHash();
/// <inheritdoc />
ReadOnlySpan<Byte> IKeyBitmapDataAccess.GetData() => new(this.rawBitmapData);
private Int32 CalculateObjectHash() {
const Int32 initalValue = 17;
const Int32 primeFactor = 23;
const Int32 imageSampleSize = 1000;
unchecked {
Int32 hash = initalValue;
hash = (hash * primeFactor) + this.Width;
hash = (hash * primeFactor) + this.Height;
if(this.rawBitmapData.Length == 0) {
return hash;
}
Int32 stepSize = 1;
if(this.rawBitmapData.Length > imageSampleSize) {
stepSize = this.rawBitmapData.Length / imageSampleSize;
}
for(Int32 i = 0; i < this.rawBitmapData.Length; i += stepSize) {
hash *= 23;
hash += this.rawBitmapData[i];
}
return hash;
}
}
}
}

View File

@ -0,0 +1,249 @@
using SixLabors.ImageSharp;
using System;
using System.IO;
using System.Threading.Tasks;
namespace OpenMacroBoard.SDK {
/// <summary>
/// Collection of factory extension methods for <see cref="KeyBitmap"/>s.
/// </summary>
/// <remarks>
/// <para>You typically don't want to invoke the static methods directly, but instead use them
/// as extension methods by calling <see cref="KeyBitmap.Create"/>.XYZ();</para>
/// </remarks>
public static class KeyBitmapBasicFactoryExtensions {
/// <summary>
/// Creates a new <see cref="KeyBitmap"/> object.
/// </summary>
/// <param name="keyFactory">The builder that is used to create the <see cref="KeyBitmap"/></param>
/// <param name="width">width of the bitmap</param>
/// <param name="height">height of the bitmap</param>
/// <param name="bitmapDataBgr24">raw bitmap data (Bgr24)</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Either <paramref name="width"/> or <paramref name="height"/> are smaller than one.
/// </exception>
/// <exception cref="ArgumentException">
/// Provided <paramref name="width"/> and <paramref name="height"/> doesn't match the
/// expected array length of <paramref name="bitmapDataBgr24"/>.
/// </exception>
public static KeyBitmap FromBgr24Array(
this IKeyBitmapFactory keyFactory,
int width,
int height,
ReadOnlySpan<byte> bitmapDataBgr24
) {
if(width < 1) {
throw new ArgumentOutOfRangeException(nameof(width));
}
if(height < 1) {
throw new ArgumentOutOfRangeException(nameof(height));
}
var expectedLength = width * height * 3;
if(bitmapDataBgr24.Length != expectedLength) {
throw new ArgumentException($"{nameof(bitmapDataBgr24)}.Length does not match it's expected size ({nameof(width)} x {nameof(height)} x 3)", nameof(bitmapDataBgr24));
}
var data = new byte[expectedLength];
bitmapDataBgr24.CopyTo(data);
return new KeyBitmap(width, height, data);
}
/// <summary>
/// Creates a new <see cref="KeyBitmap"/> object.
/// </summary>
/// <param name="keyFactory">The builder that is used to create the <see cref="KeyBitmap"/></param>
/// <param name="width">width of the bitmap</param>
/// <param name="height">height of the bitmap</param>
/// <param name="bitmapDataBgra32">raw bitmap data (Bgra32). The alpha channel will be ignored.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Either <paramref name="width"/> or <paramref name="height"/> are smaller than one.
/// </exception>
/// <exception cref="ArgumentException">
/// Provided <paramref name="width"/> and <paramref name="height"/> doesn't match the
/// expected array length of <paramref name="bitmapDataBgra32"/>.
/// </exception>
public static KeyBitmap FromBgra32Array(
this IKeyBitmapFactory keyFactory,
int width,
int height,
ReadOnlySpan<byte> bitmapDataBgra32
) {
if(width < 1) {
throw new ArgumentOutOfRangeException(nameof(width));
}
if(height < 1) {
throw new ArgumentOutOfRangeException(nameof(height));
}
var pixelCount = width * height;
var expectedLength = pixelCount * 4;
if(bitmapDataBgra32.Length != expectedLength) {
throw new ArgumentException($"{nameof(bitmapDataBgra32)}.Length does not match it's expected size ({nameof(width)} x {nameof(height)} x 4)", nameof(bitmapDataBgra32));
}
var data = new byte[expectedLength];
for(int i = 0; i < pixelCount; i++) {
var i3 = i * 3;
var i4 = i * 4;
// Copy BGR and ignore alpha channel
data[i3 + 0] = bitmapDataBgra32[i4 + 0];
data[i3 + 1] = bitmapDataBgra32[i4 + 1];
data[i3 + 2] = bitmapDataBgra32[i4 + 2];
}
return new KeyBitmap(width, height, data);
}
/// <summary>
/// Creates a new <see cref="KeyBitmap"/> object.
/// </summary>
/// <param name="keyFactory">The builder that is used to create the <see cref="KeyBitmap"/></param>
/// <param name="width">width of the bitmap</param>
/// <param name="height">height of the bitmap</param>
/// <param name="bitmapDataRgba32">raw bitmap data (Bgra32). The alpha channel will be ignored.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Either <paramref name="width"/> or <paramref name="height"/> are smaller than one.
/// </exception>
/// <exception cref="ArgumentException">
/// Provided <paramref name="width"/> and <paramref name="height"/> doesn't match the
/// expected array length of <paramref name="bitmapDataRgba32"/>.
/// </exception>
public static KeyBitmap FromRgba32Array(
this IKeyBitmapFactory keyFactory,
int width,
int height,
ReadOnlySpan<byte> bitmapDataRgba32
) {
if(width < 1) {
throw new ArgumentOutOfRangeException(nameof(width));
}
if(height < 1) {
throw new ArgumentOutOfRangeException(nameof(height));
}
var pixelCount = width * height;
var expectedLength = pixelCount * 4;
if(bitmapDataRgba32.Length != expectedLength) {
throw new ArgumentException($"{nameof(bitmapDataRgba32)}.Length does not match it's expected size ({nameof(width)} x {nameof(height)} x 4)", nameof(bitmapDataRgba32));
}
var data = new byte[expectedLength];
for(int i = 0; i < pixelCount; i++) {
var i3 = i * 3;
var i4 = i * 4;
// Copy BGR and ignore alpha channel
data[i3 + 2] = bitmapDataRgba32[i4 + 0];
data[i3 + 1] = bitmapDataRgba32[i4 + 1];
data[i3 + 0] = bitmapDataRgba32[i4 + 2];
}
return new KeyBitmap(width, height, data);
}
/// <summary>
/// Creates a new <see cref="KeyBitmap"/> object.
/// </summary>
/// <param name="keyFactory">The builder that is used to create the <see cref="KeyBitmap"/></param>
/// <param name="width">width of the bitmap</param>
/// <param name="height">height of the bitmap</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Either <paramref name="width"/> or <paramref name="height"/> are smaller than one.
/// </exception>
public static KeyBitmap Empty(
this IKeyBitmapFactory keyFactory,
int width,
int height
) {
if(width < 1) {
throw new ArgumentOutOfRangeException(nameof(width));
}
if(height < 1) {
throw new ArgumentOutOfRangeException(nameof(height));
}
return new KeyBitmap(width, height, Array.Empty<byte>());
}
/// <summary>
/// Creates a single color (single pixel) <see cref="KeyBitmap"/> with a given color.
/// </summary>
/// <param name="keyFactory">The builder that is used to create the <see cref="KeyBitmap"/></param>
/// <param name="r">Red channel.</param>
/// <param name="g">Green channel.</param>
/// <param name="b">Blue channel.</param>
public static KeyBitmap FromRgb(this IKeyBitmapFactory keyFactory, byte r, byte g, byte b) {
// If everything is 0 (black) take a shortcut ;-)
if(r == 0 && g == 0 && b == 0) {
return KeyBitmap.Black;
}
var buffer = new byte[3] { b, g, r };
return KeyBitmap.Create.FromBgr24Array(1, 1, buffer);
}
/// <summary>
/// Creates a single color (single pixel) <see cref="KeyBitmap"/> with a given color.
/// </summary>
/// <param name="keyFactory">The builder that is used to create the <see cref="KeyBitmap"/></param>
/// <param name="color">The color.</param>
public static KeyBitmap FromColor(this IKeyBitmapFactory keyFactory, OmbColor color) {
return keyFactory.FromRgb(color.R, color.G, color.B);
}
/// <summary>
/// Create a bitmap from an encoded given image stream.
/// </summary>
public static KeyBitmap FromStream(this IKeyBitmapFactory builder, Stream bitmapStream) {
return builder.FromImageSharpImage(Image.Load(bitmapStream));
}
/// <summary>
/// Create a bitmap from an encoded given image stream.
/// </summary>
public static async Task<KeyBitmap> FromStreamAsync(this IKeyBitmapFactory builder, Stream bitmapStream) {
return builder.FromImageSharpImage(await Image.LoadAsync(bitmapStream));
}
/// <summary>
/// Create a bitmap from an encoded given image file.
/// </summary>
public static KeyBitmap FromFile(this IKeyBitmapFactory builder, string bitmapFile) {
return builder.FromImageSharpImage(Image.Load(bitmapFile));
}
/// <summary>
/// Create a bitmap from an encoded given image file.
/// </summary>
public static async Task<KeyBitmap> FromFileAsync(this IKeyBitmapFactory builder, string bitmapFile) {
return builder.FromImageSharpImage(await Image.LoadAsync(bitmapFile));
}
/// <summary>
/// Creates a <see cref="KeyBitmap"/> from a given <see cref="Image{Bgr24}"/>.
/// </summary>
/// <exception cref="ArgumentNullException">The provided bitmap is null.</exception>
/// <exception cref="NotSupportedException">The pixel format of the image is not supported.</exception>
public static KeyBitmap FromImageSharpImage(this IKeyBitmapFactory keyFactory, Image image) {
if(image is null) {
throw new ArgumentNullException(nameof(image));
}
using var ctx = image.WithBgr24();
var pixelData = ctx.Item.ToBgr24PixelArray();
return KeyBitmap.Create.FromBgr24Array(image.Width, image.Height, pixelData);
}
}
}

View File

@ -0,0 +1,24 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Extension methods for <see cref="IKeyBitmapDataAccess"/>.
/// </summary>
public static class KeyBitmapDataAccessExtensions
{
/// <summary>
/// Creates a new <see cref="Image{Bgr24}"/> for this <see cref="IKeyBitmapDataAccess"/>.
/// </summary>
/// <remarks>
/// <para>
/// Keep in mind that this operation allocates and creates a copy under the hood.
/// </para>
/// </remarks>
public static Image<Bgr24> ToImage(this IKeyBitmapDataAccess dataAccess)
{
return Image.LoadPixelData<Bgr24>(dataAccess.GetData(), dataAccess.Width, dataAccess.Height);
}
}
}

View File

@ -0,0 +1,107 @@
using System;
namespace OpenMacroBoard.SDK {
/// <summary>
/// An event argument that is used to communicate key state changes.
/// </summary>
public class KeyEventArgs : EventArgs {
/// <summary>
/// Initializes a new instance of the <see cref="KeyEventArgs"/> class.
/// </summary>
/// <param name="key">The index of the key that was pressed or released.</param>
/// <param name="isDown">A flag that determines if the key was pressed or released.</param>
public KeyEventArgs(Int32 key, Boolean isDown) {
this.Key = key;
this.IsDown = isDown;
}
/// <summary>
/// The index of the key that was pressed or released.
/// </summary>
public Int32 Key {
get;
}
/// <summary>
/// A flag that determines if the key was pressed or released.
/// </summary>
public Boolean IsDown {
get;
}
}
public class KeyPressEvent : EventArgs {
public KeyPressEvent(Int32 key) => this.Key = key;
public Int32 Key {
get;
}
public override String ToString() => "Key: " + this.Key;
}
public class TouchTipEvent : EventArgs {
public TouchTipEvent(UInt16 x, UInt16 y) {
this.XCoord = x;
this.YCoord = y;
}
public UInt16 XCoord {
get;
}
public UInt16 YCoord {
get;
}
public override String ToString() => "X-Coord: " + this.XCoord + ", Y-Coord: " + this.YCoord;
}
public class TouchFlipEvent : EventArgs {
public TouchFlipEvent(UInt16 fx, UInt16 fy, UInt16 tx, UInt16 ty) {
this.FromXCoord = fx;
this.FromYCoord = fy;
this.ToXCoord = tx;
this.ToYCoord = ty;
}
public UInt16 FromXCoord {
get;
}
public UInt16 FromYCoord {
get;
}
public UInt16 ToXCoord {
get;
}
public UInt16 ToYCoord {
get;
}
public override String ToString() => "From X-Coord: " + this.FromXCoord + ", Y-Coord: " + this.FromYCoord+ ", To X-Coord: " + this.ToXCoord + ", Y-Coord: " + this.ToYCoord;
}
public class KnobPressEvent : EventArgs {
public KnobPressEvent(Int32 key) => this.Encoder = key;
public Int32 Encoder {
get;
}
public override String ToString() => "Encoder: " + this.Encoder;
}
public class KnobTurnEvent : EventArgs {
public KnobTurnEvent(Int32 key, SByte steps) {
this.Encoder = key;
this.Steps = steps;
}
public Int32 Encoder {
get;
}
public SByte Steps {
get;
}
public override String ToString() => "Encoder: " + this.Encoder + ", Steps: " + this.Steps;
}
}

View File

@ -0,0 +1,33 @@
using System;
namespace OpenMacroBoard.SDK.Helper
{
/// <summary>
/// Extensions from <see cref="IKeyLayout"/>s.
/// </summary>
public static class KeyLayoutAreaHelperExtensions
{
/// <summary>
/// Calculates a <see cref="OmbRectangle"/> which spans all keys from a given <see cref="IKeyLayout"/>.
/// </summary>
/// <param name="keyLayout">The key layout this area is calculated for.</param>
/// <returns>Returns a rectangle that spans all keys.</returns>
public static OmbRectangle GetFullArea(this IKeyLayout keyLayout)
{
var minX = 0;
var minY = 0;
var maxX = 0;
var maxY = 0;
foreach (var keyRect in keyLayout)
{
minX = Math.Min(minX, keyRect.Left);
minY = Math.Min(minY, keyRect.Top);
maxX = Math.Max(maxX, keyRect.Right);
maxY = Math.Max(maxY, keyRect.Bottom);
}
return new OmbRectangle(minX, minY, maxX - minX, maxY - minY);
}
}
}

View File

@ -0,0 +1,144 @@
using System;
using System.Reflection.PortableExecutable;
namespace OpenMacroBoard.SDK {
/// <summary>
/// Wraps an <see cref="IMacroBoard"/> and allows for hooks to implement "middle-ware" features.
/// </summary>
public abstract class MacroBoardAdapter : IMacroBoard {
private readonly IMacroBoard macroBoard;
private readonly Boolean leaveOpen;
private Boolean disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="MacroBoardAdapter"/> class.
/// </summary>
/// <remarks>
/// <para>When this instance is disposed, the underlying board is disposed as well.</para>
/// </remarks>
/// <param name="macroBoard">The macroBoard that is wrapped.</param>
protected MacroBoardAdapter(IMacroBoard macroBoard) : this(macroBoard, false) {
}
/// <summary>
/// Initializes a new instance of the <see cref="MacroBoardAdapter"/> class.
/// </summary>
/// <param name="macroBoard">The macroBoard that is wrapped.</param>
/// <param name="leaveOpen">When true, the underlying macroBoard will not be disposed with this instance.</param>
protected MacroBoardAdapter(IMacroBoard macroBoard, Boolean leaveOpen) {
this.macroBoard = macroBoard ?? throw new ArgumentNullException(nameof(macroBoard));
this.leaveOpen = leaveOpen;
macroBoard.ConnectionStateChanged += this.OnConnectionStateChanged;
//macroBoard.KeyStateChanged += this.OnKeyStateChanged;
macroBoard.ButtonPressed += this.MacroBoard_ButtonPressed;
macroBoard.ButtonReleased += this.MacroBoard_ButtonReleased;
macroBoard.TouchbarTouched += this.MacroBoard_TouchbarTouched;
macroBoard.TouchbarPressed += this.MacroBoard_TouchbarPressed;
macroBoard.TouchFlipEvent += this.MacroBoard_TouchFlipEvent;
macroBoard.EncoderPressed += this.MacroBoard_EncoderPressed;
macroBoard.EncoderReleased += this.MacroBoard_EncoderReleased;
macroBoard.EncoderTurn += this.MacroBoard_EncoderTurn;
}
/// <summary>
/// Finalizes an instance of the <see cref="MacroBoardAdapter"/> class.
/// </summary>
~MacroBoardAdapter() {
this.Dispose(false);
}
/// <inheritdoc/>
//public event EventHandler<KeyEventArgs> KeyStateChanged;
public event EventHandler<KeyPressEvent> ButtonPressed;
public event EventHandler<KeyPressEvent> ButtonReleased;
public event EventHandler<TouchTipEvent> TouchbarTouched;
public event EventHandler<TouchTipEvent> TouchbarPressed;
public event EventHandler<TouchFlipEvent> TouchFlipEvent;
public event EventHandler<KnobPressEvent> EncoderPressed;
public event EventHandler<KnobPressEvent> EncoderReleased;
public event EventHandler<KnobTurnEvent> EncoderTurn;
/// <inheritdoc/>
public event EventHandler<ConnectionEventArgs> ConnectionStateChanged;
/// <inheritdoc/>
public virtual IKeyLayout Keys => this.macroBoard.Keys;
/// <inheritdoc/>
public virtual Boolean IsConnected => this.macroBoard.IsConnected;
/// <inheritdoc/>
public void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public virtual void SetBrightness(Byte percent) => this.macroBoard.SetBrightness(percent);
/// <inheritdoc/>
public virtual void SetButtonImage(Int32 keyId, KeyBitmap bitmapData) => this.macroBoard.SetButtonImage(keyId, bitmapData);
/// <inheritdoc/>
public void SetFullScreenImage(KeyBitmap bitmapData) => throw new NotImplementedException();
/// <inheritdoc/>
public virtual void SetWindowImage(KeyBitmap bitmapData) => throw new NotImplementedException();
/// <inheritdoc/>
public virtual void SetPartialWindowImage(Int32 x_pos, Int32 y_pos, KeyBitmap bitmapData) => throw new NotImplementedException();
/// <inheritdoc/>
public virtual void ShowLogo() => this.macroBoard.ShowLogo();
/// <inheritdoc/>
public String GetFirmwareVersion() => this.macroBoard.GetFirmwareVersion();
/// <inheritdoc/>
public String GetSerialNumber() => this.macroBoard.GetFirmwareVersion();
// <summary>
// Virtual KeyStateChanged event handler.
// </summary>
// <param name="sender">The sender of the original event.</param>
// <param name="e">The event arguments.</param>
//protected virtual void OnKeyStateChanged(Object sender, KeyEventArgs e) => KeyStateChanged?.Invoke(this, e);
private void MacroBoard_EncoderTurn(Object sender, KnobTurnEvent e) => this.EncoderTurn?.Invoke(this, e);
private void MacroBoard_EncoderReleased(Object sender, KnobPressEvent e) => this.EncoderReleased?.Invoke(this, e);
private void MacroBoard_EncoderPressed(Object sender, KnobPressEvent e) => this.EncoderPressed?.Invoke(this, e);
private void MacroBoard_TouchFlipEvent(Object sender, TouchFlipEvent e) => this.TouchFlipEvent?.Invoke(this, e);
private void MacroBoard_TouchbarPressed(Object sender, TouchTipEvent e) => this.TouchbarPressed?.Invoke(this, e);
private void MacroBoard_TouchbarTouched(Object sender, TouchTipEvent e) => this.TouchbarTouched?.Invoke(this, e);
private void MacroBoard_ButtonReleased(Object sender, KeyPressEvent e) => this.ButtonReleased?.Invoke(this, e);
private void MacroBoard_ButtonPressed(Object sender, KeyPressEvent e) => this.ButtonPressed?.Invoke(this, e);
/// <summary>
/// Virtual ConnectionStateChanged event handler.
/// </summary>
/// <param name="sender">The sender of the original event.</param>
/// <param name="e">The event arguments.</param>
protected virtual void OnConnectionStateChanged(Object sender, ConnectionEventArgs e) => ConnectionStateChanged?.Invoke(this, e);
/// <summary>
/// Protected implementation of Dispose pattern.
/// </summary>
/// <param name="disposing">True when called from <see cref="Dispose()"/> and false when called from the finalizer.</param>
protected virtual void Dispose(Boolean disposing) {
if(this.disposed) {
return;
}
if(disposing && !this.leaveOpen) {
this.macroBoard?.Dispose();
}
this.disposed = true;
}
}
}

View File

@ -0,0 +1,47 @@
using System;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// Extensions for <see cref="IMacroBoard"/> enrichment ;-)
/// </summary>
public static class MacroBoardFeatureExtensions
{
/// <summary>
/// Wraps an <see cref="IMacroBoard"/> with an button press effect adapter.
/// </summary>
/// <param name="macroBoard">The board that should be wrapped.</param>
/// <param name="config">The configuration that should be used. Changes to the configuration later also takes effect.</param>
/// <returns>Returns a new board that implements the button press effect.</returns>
/// <exception cref="ArgumentNullException">The provided board is null.</exception>
public static IMacroBoard WithButtonPressEffect(this IMacroBoard macroBoard, ButtonPressEffectConfig config = null)
{
if (macroBoard is null)
{
throw new ArgumentNullException(nameof(macroBoard));
}
return new ButtonPressEffectAdapter(macroBoard, config);
}
/// <summary>
/// Wraps an <see cref="IMacroBoard"/> with a disconnect replay adapter.
/// </summary>
/// <param name="macroBoard">The board that should be wrapped.</param>
/// <returns>Returns a new board that implements the replay feature.</returns>
/// <remarks>
/// <para>This adapter makes sure, that if a device is disconnected that previously set properties like
/// images and brightness are replayed if the device is connected again.</para>
/// </remarks>
/// <exception cref="ArgumentNullException">The provided board is null.</exception>
public static IMacroBoard WithDisconnectReplay(this IMacroBoard macroBoard)
{
if (macroBoard is null)
{
throw new ArgumentNullException(nameof(macroBoard));
}
return new DisconnectReplayAdapter(macroBoard);
}
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<!-- nuget stuff -->
<PropertyGroup>
<Version>7.0.0</Version>
<Title>OpenMacroBoard.SDK</Title>
<Description>Abstraction for macro boards (with LCD keys)</Description>
<RepositoryUrl>https://github.com/OpenMacroBoard/OpenMacroBoard.SDK</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\ImageSharp\ImageSharp\ImageSharp.csproj" />
</ItemGroup>
<!--<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
<PackageReference Include="System.Memory" Version="4.6.3" />
</ItemGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
<DefineConstants>$(DefineConstants);SUPPORTS_HASHCODE</DefineConstants>
</PropertyGroup>-->
</Project>

View File

@ -0,0 +1,49 @@
using System;
namespace OpenMacroBoard.SDK
{
/// <summary>
/// A bunch of extensions to clear all keys, or set a single <see cref="KeyBitmap"/> to all keys.
/// </summary>
public static class SetKeyExtensions
{
/// <summary>
/// Sets a background image for all keys
/// </summary>
/// <exception cref="ArgumentNullException">The provided board is null.</exception>
public static void SetKeyBitmap(this IMacroBoard board, KeyBitmap bitmap)
{
if (board is null)
{
throw new ArgumentNullException(nameof(board));
}
for (var i = 0; i < board.Keys.Count; i++)
{
board.SetButtonImage(i, bitmap);
}
}
/// <summary>
/// Sets background to black for a given key
/// </summary>
/// <exception cref="ArgumentNullException">The provided board is null.</exception>
public static void ClearKey(this IMacroBoard board, int keyId)
{
if (board is null)
{
throw new ArgumentNullException(nameof(board));
}
board.SetButtonImage(keyId, KeyBitmap.Black);
}
/// <summary>
/// Sets background to black for all given keys
/// </summary>
public static void ClearKeys(this IMacroBoard board)
{
board.SetKeyBitmap(KeyBitmap.Black);
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
namespace OpenMacroBoard.SDK.Utils
{
/// <summary>
/// Some LINQ extensions we use internally.
/// </summary>
public static class LinqExtensions
{
/// <summary>
/// <para>
/// A useful combination of LINQs Select() and Where().
/// Prevents invalid object states between those two calls by combining them.
/// </para>
/// <para>
/// If the <paramref name="selector"/> returns <c>(true, value)</c> the value will be yielded.
/// If the <paramref name="selector"/> returns <c>(false, value)</c> it will not be present in
/// the ouput enumerble.
/// </para>
/// </summary>
/// <typeparam name="TIn">Input type.</typeparam>
/// <typeparam name="TOut">Output type.</typeparam>
/// <param name="source">Incomming source enumerable</param>
/// <param name="selector">Filter/Selector</param>
public static IEnumerable<TOut> SelectWhere<TIn, TOut>(
this IEnumerable<TIn> source,
Func<TIn, (bool Success, TOut Output)> selector
)
{
foreach (var item in source)
{
var (success, output) = selector(item);
if (success)
{
yield return output;
}
}
}
}
}

BIN
OpenMacroBoard.SDK/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

40
StreamDeckSharp.sln Normal file
View File

@ -0,0 +1,40 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.5.11716.220 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamDeckSharp", "StreamDeckSharp\StreamDeckSharp.csproj", "{1C90BA5B-09A9-75A5-5FC4-59C154B641D1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMacroBoard.SDK", "OpenMacroBoard.SDK\OpenMacroBoard.SDK.csproj", "{3EF6B61C-551F-335B-8E30-F53E832F037A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp", "..\ImageSharp\ImageSharp\ImageSharp.csproj", "{144E1462-962C-E752-C3F9-2ECD13D95F6E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1C90BA5B-09A9-75A5-5FC4-59C154B641D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C90BA5B-09A9-75A5-5FC4-59C154B641D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C90BA5B-09A9-75A5-5FC4-59C154B641D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C90BA5B-09A9-75A5-5FC4-59C154B641D1}.Release|Any CPU.Build.0 = Release|Any CPU
{3EF6B61C-551F-335B-8E30-F53E832F037A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3EF6B61C-551F-335B-8E30-F53E832F037A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3EF6B61C-551F-335B-8E30-F53E832F037A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3EF6B61C-551F-335B-8E30-F53E832F037A}.Release|Any CPU.Build.0 = Release|Any CPU
{144E1462-962C-E752-C3F9-2ECD13D95F6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{144E1462-962C-E752-C3F9-2ECD13D95F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{144E1462-962C-E752-C3F9-2ECD13D95F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{144E1462-962C-E752-C3F9-2ECD13D95F6E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {897E8FA3-2A42-46E7-B2A3-4213DA35864D}
EndGlobalSection
GlobalSection(SharedMSBuildProjectFiles) = preSolution
..\ImageSharp\SharedInfrastructure.projitems*{144e1462-962c-e752-c3f9-2ecd13d95f6e}*SharedItemsImports = 5
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StreamDeckSharp.Tests")]

View File

@ -0,0 +1,37 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace StreamDeckSharp.Exceptions {
/// <summary>
/// Base class for all StreamDeckSharp Exceptions
/// </summary>
[Serializable]
[ExcludeFromCodeCoverage]
public abstract class StreamDeckException : Exception {
/// <summary>
/// Initializes a new instance of the <see cref="StreamDeckException"/> class.
/// </summary>
protected StreamDeckException() {
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamDeckException"/> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
protected StreamDeckException(String message)
: base(message) {
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamDeckException"/> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a null reference
/// if no inner exception is specified.
/// </param>
protected StreamDeckException(String message, Exception innerException)
: base(message, innerException) {
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace StreamDeckSharp.Exceptions {
/// <summary>
/// Is thrown if no device could be found
/// </summary>
[Serializable]
[ExcludeFromCodeCoverage]
public class StreamDeckNotFoundException
: StreamDeckException {
/// <summary>
/// Initializes a new instance of the <see cref="StreamDeckNotFoundException"/> class.
/// </summary>
internal StreamDeckNotFoundException()
: base("Stream Deck not found.") {
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamDeckNotFoundException"/> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
internal StreamDeckNotFoundException(String message)
: base(message) {
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamDeckNotFoundException"/> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a null reference
/// if no inner exception is specified.
/// </param>
internal StreamDeckNotFoundException(String message, Exception innerException)
: base(message, innerException) {
}
}
}

215
StreamDeckSharp/Hardware.cs Normal file
View File

@ -0,0 +1,215 @@
using OpenMacroBoard.SDK;
using StreamDeckSharp.Internals;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using static StreamDeckSharp.UsbConstants;
#pragma warning disable AV1710 // Member name includes the name of its containing type
namespace StreamDeckSharp {
/// <summary>
/// Details about different StreamDeck Hardware
/// </summary>
public static class Hardware {
private static readonly ConcurrentDictionary<UsbVendorProductPair, UsbHardwareIdAndDriver> RegisteredHardware = new();
static Hardware() {
// .-------------.
// | Stream Deck |
// '-------------'
GridKeyLayout streamDeckKeys = new GridKeyLayout(5, 3, 72, 30);
StreamDeck =
RegisterNewHardwareInternal(
"Stream Deck",
streamDeckKeys,
new HidComDriverStreamDeck() {
// Limit of 3'200'000 bytes/s (~3.0 MiB/s)
// because without that limit glitches will happen on fast writes.
BytesPerSecondLimit = 3_200_000,
},
ElgatoUsbId(0x0060)
);
GridKeyLayout streamDeckKeysNew = new GridKeyLayout(5, 3, 72, 32);
StreamDeckMK2 =
RegisterNewHardwareInternal(
"Stream Deck MK.2",
streamDeckKeysNew,
new HidComDriverStreamDeckJpeg(72, 0, 0, 480, 272) {
// Limit of 1'500'000 bytes/s (~1.5 MB/s),
// because ImageGlitchTest.Rainbow has glitches with higher speeds
BytesPerSecondLimit = 1_500_000,
},
ElgatoUsbId(0x0080)
);
StreamDeckRev2 =
RegisterNewHardwareInternal(
"Stream Deck Rev2",
streamDeckKeysNew,
new HidComDriverStreamDeckJpeg(72, 0, 0, 480, 272) {
// Limit of 3'200'000 bytes/s (~3.0 MiB/s) just to be safe,
// because I don't own a StreamDeck Rev2 to test it.
BytesPerSecondLimit = 3_200_000,
},
ElgatoUsbId(0x006d)
);
// .----------------.
// | Stream Deck XL |
// '----------------'
StreamDeckXL =
RegisterNewHardwareInternal(
"Stream Deck XL",
new GridKeyLayout(8, 4, 96, 38),
new HidComDriverStreamDeckJpeg(96, 0, 0, 1024, 600),
ElgatoUsbId(0x006c),
ElgatoUsbId(0x008f),
ElgatoUsbId(0x00ba)
);
// .------------------.
// | Stream Deck Mini |
// '------------------'
StreamDeckMini =
RegisterNewHardwareInternal(
"Stream Deck Mini",
new GridKeyLayout(3, 2, 80, 32),
new HidComDriverStreamDeckMini(80),
ElgatoUsbId(0x0063),
ElgatoUsbId(0x0090)
);
// .------------------.
// | Stream Deck XL + |
// '------------------'
StreamDeckXLPlus =
RegisterNewHardwareInternal(
"Stream Deck XL +",
new GridKeyLayout(9, 4, 112, 32),
new HidComDriverStreamDeckJpeg(112, 1200, 100, 1280, 800),
ElgatoUsbId(0x00c6)
);
}
/// <summary>
/// Details about the classic Stream Deck
/// </summary>
public static IUsbHidHardware StreamDeck {
get;
}
/// <summary>
/// Details about the updated Stream Deck MK.2
/// </summary>
public static IUsbHidHardware StreamDeckMK2 {
get;
}
/// <summary>
/// Details about the classic Stream Deck Rev 2
/// </summary>
public static IUsbHidHardware StreamDeckRev2 {
get;
}
/// <summary>
/// Details about the Stream Deck XL
/// </summary>
public static IUsbHidHardware StreamDeckXL {
get;
}
/// <summary>
/// Details about the Stream Deck Mini
/// </summary>
public static IUsbHidHardware StreamDeckMini {
get;
}
/// <summary>
/// Details about the Stream Deck XL +
/// </summary>
public static IUsbHidHardware StreamDeckXLPlus {
get;
}
/// <summary>
/// This method registers a new (currently unknown to this library) hardware driver.
/// </summary>
/// <remarks>
/// <para>
/// This method can be used if a new stream deck hardware is released to the market and
/// the library currently doesn't have support for that new device. In the past a new device
/// was often pretty similar to an existing device so with this method a tech-savvy person
/// can register that new device.
/// </para>
/// <para>
/// This feature is a bit "low-level", just take a look at the source code
/// if you are not sure what to do.
/// </para>
/// </remarks>
/// <param name="usbId">The USB vendor and product ID.</param>
/// <param name="deviceName">A human readable name of the device.</param>
/// <param name="keyLayout">The key layout of the device.</param>
/// <param name="driver">The code that is used to communicate to the device.</param>
/// <returns>
/// Returns a description of the device that can be used to open that device with
/// <see cref="StreamDeck.OpenDevice(IUsbHidHardware[])"/> or
/// <see cref="StreamDeck.EnumerateDevices(IUsbHidHardware[])"/>.
/// </returns>
public static IUsbHidHardware RegisterNewHardware(
UsbVendorProductPair usbId,
string deviceName,
GridKeyLayout keyLayout,
IStreamDeckHidComDriver driver
) {
return RegisterNewHardwareInternal(
deviceName,
keyLayout,
driver,
usbId
);
}
internal static UsbHardwareIdAndDriver RegisterNewHardwareInternal(
string deviceName,
GridKeyLayout keyLayout,
IStreamDeckHidComDriver driver,
params UsbVendorProductPair[] usbIds
) {
UsbHardwareIdAndDriver internalReference = new UsbHardwareIdAndDriver(
usbIds.ToList(),
deviceName,
keyLayout,
driver
);
foreach(UsbVendorProductPair id in usbIds) {
RegisteredHardware.AddOrUpdate(id, internalReference, (_, _) => internalReference);
}
return internalReference;
}
internal static IEnumerable<UsbHardwareIdAndDriver> GetInternalStreamDeckHardwareInfos() {
return RegisteredHardware.Values.Distinct().ToList();
}
internal static UsbHardwareIdAndDriver GetInternalHardwareInfos(UsbVendorProductPair usbId) {
if(RegisteredHardware.TryGetValue(usbId, out UsbHardwareIdAndDriver hardwareInfo)) {
return hardwareInfo;
}
return null;
}
}
}

View File

@ -0,0 +1,22 @@
using OpenMacroBoard.SDK;
namespace StreamDeckSharp {
/// <summary>
/// A compact collection of hardware specific information about a device.
/// </summary>
public interface IHardware {
/// <summary>
/// Key layout information
/// </summary>
GridKeyLayout Keys {
get;
}
/// <summary>
/// Name of the device
/// </summary>
string DeviceName {
get;
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace StreamDeckSharp {
/// <summary>
/// USB HID specific hardware information
/// </summary>
public interface IUsbHidHardware : IHardware {
/// <summary>
/// Unique identifier for USB device. Vendor and product ID pair.
/// </summary>
IReadOnlyList<UsbVendorProductPair> UsbIds {
get;
}
}
}

View File

@ -0,0 +1,219 @@
using OpenMacroBoard.SDK;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace StreamDeckSharp.Internals {
internal class BasicHidClient : IMacroBoard {
private readonly Byte[] keyStates;
private readonly Lock disposeLock = new();
private Dictionary<Int32, Boolean> buttonstates = [];
private Dictionary<Int32, Boolean> encoderstates = [];
public BasicHidClient(IStreamDeckHid deckHid, IKeyLayout keys, IStreamDeckHidComDriver hidComDriver) {
this.DeckHid = deckHid;
this.Keys = keys;
deckHid.ConnectionStateChanged += (_, e) => ConnectionStateChanged?.Invoke(this, e);
deckHid.ReportReceived += this.DeckHid_ReportReceived;
this.HidComDriver = hidComDriver;
this.Buffer = new Byte[deckHid.OutputReportLength];
this.keyStates = new Byte[this.Keys.Count];
}
//public event EventHandler<KeyEventArgs> KeyStateChanged;
public event EventHandler<ConnectionEventArgs> ConnectionStateChanged;
public event EventHandler<KeyPressEvent> ButtonPressed;
public event EventHandler<KeyPressEvent> ButtonReleased;
public event EventHandler<TouchTipEvent> TouchbarTouched;
public event EventHandler<TouchTipEvent> TouchbarPressed;
public event EventHandler<TouchFlipEvent> TouchFlipEvent;
public event EventHandler<KnobPressEvent> EncoderPressed;
public event EventHandler<KnobPressEvent> EncoderReleased;
public event EventHandler<KnobTurnEvent> EncoderTurn;
public IKeyLayout Keys {
get;
}
public Boolean IsDisposed {
get; private set;
}
public Boolean IsConnected => this.DeckHid.IsConnected;
protected IStreamDeckHid DeckHid {
get;
}
protected IStreamDeckHidComDriver HidComDriver {
get;
}
protected Byte[] Buffer {
get;
}
public void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
}
public String GetFirmwareVersion() => this.ReadFeatureString(this.HidComDriver.FirmwareVersionFeatureId, this.HidComDriver.FirmwareVersionReportSkip);
public String GetSerialNumber() => this.ReadFeatureString(this.HidComDriver.SerialNumberFeatureId, this.HidComDriver.SerialNumberReportSkip);
public void SetBrightness(Byte percent) {
this.ThrowIfAlreadyDisposed();
_ = this.DeckHid.WriteFeature(this.HidComDriver.GetBrightnessMessage(percent));
}
public virtual void SetButtonImage(Int32 keyId, KeyBitmap bitmapData) {
keyId = this.HidComDriver.ExtKeyIdToHardwareKeyId(keyId);
Byte[] payload = this.HidComDriver.GenerateButtonImage(bitmapData);
IEnumerable<Byte[]> reports = OutputReportSplitter.ButtonImage(payload, this.Buffer, this.HidComDriver.ReportSize, this.HidComDriver.HeaderSizeButton, keyId, this.HidComDriver.PrepareHeaderOutButtonImage);
foreach(Byte[] report in reports) {
_ = this.DeckHid.WriteReport(report);
}
}
public virtual void SetFullScreenImage(KeyBitmap bitmapData) {
Byte[] payload = this.HidComDriver.GenerateFullScreenImage(bitmapData);
IEnumerable<Byte[]> reports = OutputReportSplitter.FullScreenImage(payload, this.Buffer, this.HidComDriver.ReportSize, this.HidComDriver.HeaderSizeFullScreenImage, this.HidComDriver.PrepareHeaderOutFullScreenImage);
foreach(Byte[] report in reports) {
_ = this.DeckHid.WriteReport(report);
}
}
public virtual void SetWindowImage(KeyBitmap bitmapData) {
Byte[] payload = this.HidComDriver.GenerateWindowImage(bitmapData);
IEnumerable<Byte[]> reports = OutputReportSplitter.WindowImage(payload, this.Buffer, this.HidComDriver.ReportSize, this.HidComDriver.HeaderSizeWindowImage, this.HidComDriver.PrepareHeaderOutWindowImage);
foreach(Byte[] report in reports) {
_ = this.DeckHid.WriteReport(report);
}
}
public virtual void SetPartialWindowImage(Int32 x_pos, Int32 y_pos, KeyBitmap bitmapData) {
Byte[] payload = this.HidComDriver.GeneratePartialWindowImage(bitmapData);
IEnumerable<Byte[]> reports = OutputReportSplitter.PartialWindowImage(payload, this.Buffer, this.HidComDriver.ReportSize, this.HidComDriver.HeaderSizePartialWindowImage, x_pos, y_pos, bitmapData.Width, bitmapData.Height, this.HidComDriver.PrepareHeaderOutPartialWindowImage);
foreach(Byte[] report in reports) {
_ = this.DeckHid.WriteReport(report);
}
}
public void ShowLogo() {
this.ThrowIfAlreadyDisposed();
this.ShowLogoWithoutDisposeVerification();
}
protected virtual void Shutdown() {
}
protected virtual void Dispose(Boolean disposing) {
if(disposing) {
lock(this.disposeLock) {
if(this.IsDisposed) {
return;
}
this.IsDisposed = true;
}
this.Shutdown();
// Sleep to let the stream deck catch up.
// Without this Sleep() the stream deck might set a key image after the logo was shown.
// I've no idea why it's sometimes executed out of order even though the write is synchronized.
Thread.Sleep(50);
this.ShowLogoWithoutDisposeVerification();
this.DeckHid.Dispose();
}
}
protected void ThrowIfAlreadyDisposed() => ObjectDisposedException.ThrowIf(this.IsDisposed, nameof(BasicHidClient));
private String ReadFeatureString(Byte featureId, Int32 skipBytes) =>
!this.DeckHid.ReadFeatureData(featureId, out Byte[] featureData)
? null
: Encoding.UTF8.GetString(featureData, skipBytes, featureData.Length - skipBytes).Trim('\0');
private void DeckHid_ReportReceived(Object sender, IReportsIn e) {
if(e is ButtonPressEvent bp) {
foreach(KeyValuePair<Int32, Boolean> item in bp.ButtonStates) {
if(!this.buttonstates.ContainsKey(item.Key)) {
this.buttonstates.Add(item.Key, item.Value);
if(item.Value) {
this.ButtonPressed?.Invoke(this, new KeyPressEvent(item.Key));
}
} else {
if(item.Value && !this.buttonstates[item.Key]) {
this.buttonstates[item.Key] = item.Value;
this.ButtonPressed?.Invoke(this, new KeyPressEvent(item.Key));
} else if(!item.Value && this.buttonstates[item.Key]) {
this.buttonstates[item.Key] = item.Value;
this.ButtonReleased?.Invoke(this, new KeyPressEvent(item.Key));
}
}
}
}
if(e is TouchTapEvent tt) {
this.TouchbarTouched?.Invoke(this, new TouchTipEvent(tt.XCoord, tt.YCoord));
}
if(e is TouchPressEvent tp) {
this.TouchbarPressed?.Invoke(this, new TouchTipEvent(tp.XCoord, tp.YCoord));
}
if(e is TouchFlickEvent tf) {
this.TouchFlipEvent?.Invoke(this, new TouchFlipEvent(tf.XCoordStart, tf.YCoordStart, tf.XCoordStop, tf.YCoordStop));
}
if(e is EncoderPressEvent ep) {
foreach(KeyValuePair<Int32, Boolean> item in ep.EncoderStates) {
if(!this.encoderstates.ContainsKey(item.Key)) {
this.encoderstates.Add(item.Key, item.Value);
if(item.Value) {
this.EncoderPressed?.Invoke(this, new KnobPressEvent(item.Key));
}
} else {
if(item.Value && !this.encoderstates[item.Key]) {
this.encoderstates[item.Key] = item.Value;
this.EncoderPressed?.Invoke(this, new KnobPressEvent(item.Key));
} else if(!item.Value && this.encoderstates[item.Key]) {
this.encoderstates[item.Key] = item.Value;
this.EncoderReleased?.Invoke(this, new KnobPressEvent(item.Key));
}
}
}
}
if(e is EncoderRotateEvent er) {
foreach(KeyValuePair<Int32, SByte> item in er.EncoderStates) {
if(item.Value != 0) {
this.EncoderTurn?.Invoke(this, new KnobTurnEvent(item.Key, item.Value));
}
}
}
//Console.WriteLine(e);
}
/*private void ProcessKeys(Byte[] newStates) {
for(Int32 i = 0; i < this.keyStates.Length; i++) {
Int32 newStatePos = i + this.HidComDriver.KeyReportOffset;
if(this.keyStates[i] != newStates[newStatePos]) {
Int32 externalKeyId = this.HidComDriver.HardwareKeyIdToExtKeyId(i);
KeyStateChanged?.Invoke(this, new KeyEventArgs(externalKeyId, newStates[newStatePos] != 0));
this.keyStates[i] = newStates[newStatePos];
}
}
}*/
private void ShowLogoWithoutDisposeVerification() => this.DeckHid.WriteFeature(this.HidComDriver.GetLogoMessage());
}
}

View File

@ -0,0 +1,113 @@
using OpenMacroBoard.SDK;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System;
namespace StreamDeckSharp.Internals {
internal class CachedHidClient : BasicHidClient {
private readonly Task writerTask;
private readonly ConcurrentBufferedQueue<OutpurReportType, Byte[]> imageQueue;
private readonly ConditionalWeakTable<KeyBitmap, Byte[]> cacheKeyBitmaps = [];
private interface OutpurReportType {}
private class ButtonImage : OutpurReportType {
public Int32 KeyId {
get; set;
}
}
private class FullScreenImage : OutpurReportType {
}
private class WindowImage : OutpurReportType {
}
private class PartialWindowImage : OutpurReportType {
public Int32 XCoord {
get; set;
}
public Int32 YCoord {
get; set;
}
public Int32 Width {
get; set;
}
public Int32 Height {
get; set;
}
}
public CachedHidClient(IStreamDeckHid deckHid, IKeyLayout keys, IStreamDeckHidComDriver hidComDriver) : base(deckHid, keys, hidComDriver) {
this.imageQueue = new ConcurrentBufferedQueue<OutpurReportType, Byte[]>();
this.writerTask = this.StartBitmapWriterTask();
}
public override void SetButtonImage(Int32 keyId, KeyBitmap bitmapData) {
this.ThrowIfAlreadyDisposed();
this.imageQueue.Add(new ButtonImage { KeyId = this.HidComDriver.ExtKeyIdToHardwareKeyId(keyId) }, this.cacheKeyBitmaps.GetValue(bitmapData, this.HidComDriver.GenerateButtonImage));
}
public override void SetFullScreenImage(KeyBitmap bitmapData) {
this.ThrowIfAlreadyDisposed();
this.imageQueue.Add(new FullScreenImage(), this.cacheKeyBitmaps.GetValue(bitmapData, this.HidComDriver.GenerateFullScreenImage));
}
public override void SetWindowImage(KeyBitmap bitmapData) {
this.ThrowIfAlreadyDisposed();
this.imageQueue.Add(new WindowImage(), this.cacheKeyBitmaps.GetValue(bitmapData, this.HidComDriver.GenerateWindowImage));
}
public override void SetPartialWindowImage(Int32 x_pos, Int32 y_pos, KeyBitmap bitmapData) {
ArgumentOutOfRangeException.ThrowIfNotEqual(y_pos, 0);
KeyBitmap scaledbitmap = this.HidComDriver.AdjustImageSize(bitmapData, x_pos, y_pos);
this.ThrowIfAlreadyDisposed();
this.imageQueue.Add(new PartialWindowImage { XCoord = x_pos, YCoord = y_pos, Width = scaledbitmap.Width, Height = scaledbitmap.Height }, this.cacheKeyBitmaps.GetValue(scaledbitmap, this.HidComDriver.GeneratePartialWindowImage));
}
protected override void Shutdown() {
this.imageQueue.CompleteAdding();
this.writerTask.Wait();
}
protected override void Dispose(Boolean disposing) {
base.Dispose(disposing);
this.imageQueue.Dispose();
}
private Task StartBitmapWriterTask() {
void BackgroundAction() {
while(true) {
(Boolean success, OutpurReportType type, Byte[] payload) = this.imageQueue.Take();
if(!success) {
// image queue completed
break;
}
IEnumerable<Byte[]> reports = [];
if(type is ButtonImage b) {
reports = OutputReportSplitter.ButtonImage(payload, this.Buffer, this.HidComDriver.ReportSize, this.HidComDriver.HeaderSizeButton, b.KeyId, this.HidComDriver.PrepareHeaderOutButtonImage);
}
if(type is FullScreenImage) {
reports = OutputReportSplitter.FullScreenImage(payload, this.Buffer, this.HidComDriver.ReportSize, this.HidComDriver.HeaderSizeFullScreenImage, this.HidComDriver.PrepareHeaderOutFullScreenImage);
}
if(type is WindowImage) {
reports = OutputReportSplitter.WindowImage(payload, this.Buffer, this.HidComDriver.ReportSize, this.HidComDriver.HeaderSizeFullScreenImage, this.HidComDriver.PrepareHeaderOutWindowImage);
}
if(type is PartialWindowImage p) {
reports = OutputReportSplitter.PartialWindowImage(payload, this.Buffer, this.HidComDriver.ReportSize, this.HidComDriver.HeaderSizePartialWindowImage, p.XCoord, p.YCoord, p.Width, p.Height, this.HidComDriver.PrepareHeaderOutPartialWindowImage);
}
foreach(Byte[] report in reports) {
_ = this.DeckHid.WriteReport(report);
}
}
}
return Task.Factory.StartNew(
BackgroundAction,
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default
);
}
}
}

View File

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace StreamDeckSharp.Internals
{
internal sealed class ConcurrentBufferedQueue<TKey, TValue> : IDisposable
{
private readonly object sync = new();
private readonly Dictionary<TKey, TValue> valueBuffer = new();
private readonly Queue<TKey> queue = new();
private volatile bool isAddingCompleted;
private volatile bool disposed;
public int Count => this.queue.Count;
public bool IsAddingCompleted
{
get
{
this.ThrowIfDisposed();
return this.isAddingCompleted;
}
}
public bool IsCompleted
{
get
{
lock (this.sync)
{
this.ThrowIfDisposed();
return this.isAddingCompleted && this.Count == 0;
}
}
}
public void Add(TKey key, TValue value)
{
lock (this.sync)
{
this.ThrowIfDisposed();
if (this.isAddingCompleted)
{
throw new InvalidOperationException("Adding was already marked as completed.");
}
try
{
this.valueBuffer[key] = value;
if (!this.queue.Contains(key))
{
this.queue.Enqueue(key);
}
}
finally
{
Monitor.PulseAll(this.sync);
}
}
}
public (bool Success, TKey Key, TValue Value) Take()
{
lock (this.sync)
{
while (this.queue.Count < 1)
{
this.ThrowIfDisposed();
if (this.isAddingCompleted)
{
return (false, default, default);
}
Monitor.Wait(this.sync);
}
this.ThrowIfDisposed();
TKey key = this.queue.Dequeue();
TValue value = this.valueBuffer[key];
this.valueBuffer.Remove(key);
return (true, key, value);
}
}
public void CompleteAdding()
{
lock (this.sync)
{
if (this.isAddingCompleted)
{
return;
}
this.isAddingCompleted = true;
Monitor.PulseAll(this.sync);
}
}
public void Dispose()
{
lock (this.sync)
{
if (this.disposed)
{
return;
}
this.disposed = true;
if (!this.isAddingCompleted)
{
this.CompleteAdding();
}
}
}
private void ThrowIfDisposed()
{
if (this.disposed)
{
throw new ObjectDisposedException(nameof(ConcurrentBufferedQueue<TKey, TValue>));
}
}
}
}

View File

@ -0,0 +1,62 @@
using HidSharp;
using OpenMacroBoard.SDK.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
namespace StreamDeckSharp.Internals
{
internal static class DeviceListExtensions
{
public static IEnumerable<StreamDeckDeviceReference> GetStreamDecks(
this DeviceList deviceList,
params IUsbHidHardware[] hardware
)
{
if (deviceList is null)
{
throw new ArgumentNullException(nameof(deviceList));
}
var matchAllKnowDevices = hardware is null || hardware.Length < 1;
(bool Success, UsbHardwareIdAndDriver Hardware) MatchingHardware(HidDevice d)
{
UsbHardwareIdAndDriver hwDetails = d.GetHardwareInformation();
if (hwDetails is null)
{
return (false, default);
}
if (matchAllKnowDevices)
{
return (true, hwDetails);
}
UsbVendorProductPair deviceUsbKey = new UsbVendorProductPair(d.VendorID, d.ProductID);
var hardwareMatches = hardware.Any(h => h.UsbIds.Contains(deviceUsbKey));
return (
hardwareMatches,
hardwareMatches ? hwDetails : default
);
}
return deviceList
.GetHidDevices()
.SelectWhere(device =>
{
(Boolean success, UsbHardwareIdAndDriver hardware) = MatchingHardware(device);
var value = success ? new { HardwareInfo = hardware, Device = device } : default;
return (success, value);
})
.Select(i => new StreamDeckDeviceReference(
i.Device.DevicePath,
i.HardwareInfo.DeviceName,
i.HardwareInfo.Keys
));
}
}
}

View File

@ -0,0 +1,153 @@
using OpenMacroBoard.SDK;
using System;
namespace StreamDeckSharp.Internals {
/// <summary>
/// HID Stream Deck communication driver for the classical Stream Deck.
/// </summary>
public sealed class HidComDriverStreamDeck : IStreamDeckHidComDriver {
private const Int32 ImgWidth = 72;
private const Int32 ColorChannels = 3;
private static readonly Byte[] BmpHeader = new Byte[]
{
0x42, 0x4d, 0xf6, 0x3c, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00,
0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0xc0, 0x3c, 0x00, 0x00, 0xc4, 0x0e,
0x00, 0x00, 0xc4, 0x0e, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
/// <inheritdoc/>
public Int32 HeaderSizeButton => 16;
/// <inheritdoc/>
public Int32 HeaderSizeFullScreenImage => throw new NotImplementedException();
/// <inheritdoc/>
public Int32 HeaderSizeWindowImage => throw new NotImplementedException();
/// <inheritdoc/>
public Int32 HeaderSizePartialWindowImage => throw new NotImplementedException();
/// <inheritdoc/>
public Int32 ReportSize => 7819;
/// <inheritdoc/>
public Int32 ExpectedFeatureReportLength => 17;
/// <inheritdoc/>
public Int32 ExpectedOutputReportLength => 8191;
/// <inheritdoc/>
public Int32 ExpectedInputReportLength => 17;
/// <inheritdoc/>
public Int32 KeyReportOffset => 1;
/// <inheritdoc/>
public Byte FirmwareVersionFeatureId => 4;
/// <inheritdoc/>
public Byte SerialNumberFeatureId => 3;
/// <inheritdoc/>
public Int32 FirmwareVersionReportSkip => 5;
/// <inheritdoc/>
public Int32 SerialNumberReportSkip => 5;
/// <inheritdoc/>
public Double BytesPerSecondLimit { get; set; } = Double.PositiveInfinity;
/// <inheritdoc/>
public Byte[] GenerateButtonImage(KeyBitmap keyBitmap) {
ReadOnlySpan<Byte> rawData = keyBitmap.GetScaledVersion(ImgWidth, ImgWidth);
Byte[] bmp = new Byte[ImgWidth * ImgWidth * 3 + BmpHeader.Length];
Array.Copy(BmpHeader, 0, bmp, 0, BmpHeader.Length);
if(rawData.Length != 0) {
for(Int32 y = 0; y < ImgWidth; y++) {
for(Int32 x = 0; x < ImgWidth; x++) {
Int32 src = (y * ImgWidth + x) * ColorChannels;
Int32 tar = (y * ImgWidth + (ImgWidth - 1 - x)) * ColorChannels + BmpHeader.Length;
bmp[tar + 0] = rawData[src + 0];
bmp[tar + 1] = rawData[src + 1];
bmp[tar + 2] = rawData[src + 2];
}
}
}
return bmp;
}
/// <inheritdoc/>
public Byte[] GenerateFullScreenImage(KeyBitmap keyBitmap) => throw new NotImplementedException();
/// <inheritdoc/>
public Byte[] GenerateWindowImage(KeyBitmap keyBitmap) => throw new NotImplementedException();
/// <inheritdoc/>
public Byte[] GeneratePartialWindowImage(KeyBitmap keyBitmap) => throw new NotImplementedException();
/// <inheritdoc/>
public KeyBitmap AdjustImageSize(KeyBitmap bitmapData, Int32 x_pos, Int32 y_pos) => throw new NotImplementedException();
/// <inheritdoc/>
public Int32 ExtKeyIdToHardwareKeyId(Int32 extKeyId) {
return FlipIdsHorizontal(extKeyId);
}
/// <inheritdoc/>
public Int32 HardwareKeyIdToExtKeyId(Int32 hardwareKeyId) {
return FlipIdsHorizontal(hardwareKeyId);
}
/// <inheritdoc/>
public void PrepareHeaderOutButtonImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Int32 keyId, Boolean isLast) {
data[0] = 2; // Report ID ?
data[1] = 1; // ?
data[2] = (Byte)(pageNumber + 1);
data[4] = (Byte)(isLast ? 1 : 0);
data[5] = (Byte)(keyId + 1);
}
public void PrepareHeaderOutFullScreenImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast) => throw new NotImplementedException();
public void PrepareHeaderOutWindowImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast) => throw new NotImplementedException();
public void PrepareHeaderOutPartialWindowImage(Byte[] data, Int32 x_pos, Int32 y_pos, Int32 width, Int32 height, Int32 pageNumber, Int32 payloadLength, Boolean isLast) => throw new NotImplementedException();
/// <inheritdoc/>
public Byte[] GetBrightnessMessage(Byte percent) {
if(percent > 100) {
throw new ArgumentOutOfRangeException(nameof(percent));
}
Byte[] buffer = new Byte[]
{
0x05, 0x55, 0xaa, 0xd1, 0x01, 0x64, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
};
buffer[5] = percent;
return buffer;
}
/// <inheritdoc/>
public Byte[] GetLogoMessage() {
return new Byte[] { 0x0B, 0x63 };
}
/// <inheritdoc/>
private static Int32 FlipIdsHorizontal(Int32 keyId) {
Int32 diff = ((keyId % 5) - 2) * -2;
return keyId + diff;
}
}
}

View File

@ -0,0 +1,263 @@
using OpenMacroBoard.SDK;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.IO;
namespace StreamDeckSharp.Internals {
/// <summary>
/// HID Stream Deck communication driver for JPEG based devices.
/// </summary>
public sealed class HidComDriverStreamDeckJpeg : IStreamDeckHidComDriver {
private readonly JpegEncoder jpgEncoder;
private Byte[] cachedNullImage = null;
/// <summary>
/// Initializes a new instance of the <see cref="HidComDriverStreamDeckJpeg"/> class.
/// </summary>
/// <param name="buttonSize">The size of the button images in pixels.</param>
/// /// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="buttonSize"/> is smaller than one.</exception>
public HidComDriverStreamDeckJpeg(Int32 buttonSize, Int32 touchWidth, Int32 touchHeight, Int32 fullscreenWidth, Int32 fullscreenHeight) {
ArgumentOutOfRangeException.ThrowIfLessThan(buttonSize, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(touchWidth, 0);
ArgumentOutOfRangeException.ThrowIfLessThan(touchHeight, 0);
ArgumentOutOfRangeException.ThrowIfLessThan(fullscreenWidth, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(fullscreenHeight, 1);
this.jpgEncoder = new JpegEncoder() {
Quality = 100,
};
this.ButtonSize = buttonSize;
this.TouchWidth = touchWidth;
this.TouchHeight = touchHeight;
this.FullscreenWidth = fullscreenWidth;
this.FullscreenHeight = fullscreenHeight;
}
public Int32 ButtonSize {
get;
}
public Int32 TouchWidth {
get;
}
public Int32 TouchHeight {
get;
}
public Int32 FullscreenWidth {
get;
}
public Int32 FullscreenHeight {
get;
}
/// <inheritdoc/>
public Int32 HeaderSizeButton => 0x08;
/// <inheritdoc/>
public Int32 HeaderSizeFullScreenImage => 0x08;
/// <inheritdoc/>
public Int32 HeaderSizeWindowImage => 0x08;
/// <inheritdoc/>
public Int32 HeaderSizePartialWindowImage => 0x10;
/// <inheritdoc/>
public Int32 ReportSize => 1024;
/// <inheritdoc/>
public Int32 ExpectedFeatureReportLength => 32;
/// <inheritdoc/>
public Int32 ExpectedOutputReportLength => 1024;
/// <inheritdoc/>
public Int32 ExpectedInputReportLength => 512;
/// <inheritdoc/>
public Int32 KeyReportOffset => 4;
/// <inheritdoc/>
public Byte FirmwareVersionFeatureId => 5;
/// <inheritdoc/>
public Byte SerialNumberFeatureId => 6;
/// <inheritdoc/>
public Int32 FirmwareVersionReportSkip => 6;
/// <inheritdoc/>
public Int32 SerialNumberReportSkip => 2;
/// <inheritdoc/>
public Double BytesPerSecondLimit { get; set; } = Double.PositiveInfinity;
/// <inheritdoc/>
public Int32 ExtKeyIdToHardwareKeyId(Int32 extKeyId) => extKeyId;
/// <inheritdoc/>
public Byte[] GenerateButtonImage(KeyBitmap keyBitmap) {
ReadOnlySpan<Byte> rawData = keyBitmap.GetScaledVersion(this.ButtonSize, this.ButtonSize);
return rawData.Length == 0 ? this.GetNullImage(this.ButtonSize, this.ButtonSize) : this.EncodeImageToJpg(rawData, this.ButtonSize, this.ButtonSize);
}
public Byte[] GenerateFullScreenImage(KeyBitmap keyBitmap) {
ReadOnlySpan<Byte> rawData = keyBitmap.GetScaledVersion(this.FullscreenWidth, this.FullscreenHeight);
return rawData.Length == 0 ? this.GetNullImage(this.FullscreenWidth, this.FullscreenHeight) : this.EncodeImageToJpg(rawData, this.FullscreenWidth, this.FullscreenHeight);
}
/// <inheritdoc/>
public Byte[] GenerateWindowImage(KeyBitmap keyBitmap) {
ReadOnlySpan<Byte> rawData = keyBitmap.GetScaledVersion(this.TouchWidth, this.TouchHeight);
return rawData.Length == 0 ? this.GetNullImage(this.TouchWidth, this.TouchHeight) : this.EncodeImageToJpg(rawData, this.TouchWidth, this.TouchHeight);
}
/// <inheritdoc/>
public Byte[] GeneratePartialWindowImage(KeyBitmap keyBitmap) => this.EncodeImageToJpg(((IKeyBitmapDataAccess)keyBitmap).GetData(), keyBitmap.Width, keyBitmap.Height);
/// <inheritdoc/>
public Int32 HardwareKeyIdToExtKeyId(Int32 hardwareKeyId) => hardwareKeyId;
/// <inheritdoc/>
public void PrepareHeaderOutButtonImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Int32 keyId, Boolean isLast) {
data[0] = 0x02;
data[1] = 0x07;
data[2] = (Byte)keyId;
data[3] = (Byte)(isLast ? 1 : 0);
data[4] = (Byte)(payloadLength & 0xff);
data[5] = (Byte)(payloadLength >> 8);
data[6] = (Byte)(pageNumber & 0xff);
data[7] = (Byte)(pageNumber >> 8);
}
/// <inheritdoc/>
public void PrepareHeaderOutFullScreenImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast) {
data[0] = 0x02;
data[1] = 0x08;
data[2] = 0x00;
data[3] = (Byte)(isLast ? 1 : 0);
data[4] = (Byte)(payloadLength & 0xff);
data[5] = (Byte)(payloadLength >> 8);
data[6] = (Byte)(pageNumber & 0xff);
data[7] = (Byte)(pageNumber >> 8);
}
/// <inheritdoc/>
public void PrepareHeaderOutWindowImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast) {
data[0] = 0x02;
data[1] = 0x0B;
data[2] = 0x00;
data[3] = (Byte)(isLast ? 1 : 0);
data[4] = (Byte)(payloadLength & 255);
data[5] = (Byte)(payloadLength >> 8);
data[6] = (Byte)(pageNumber & 0xff);
data[7] = (Byte)(pageNumber >> 8);
}
/// <inheritdoc/>
public void PrepareHeaderOutPartialWindowImage(Byte[] data, Int32 x_pos, Int32 y_pos, Int32 width, Int32 height, Int32 pageNumber, Int32 payloadLength, Boolean isLast) {
data[0] = 0x02;
data[1] = 0x0c;
data[2] = (Byte)(x_pos & 0xff); //2 xpos low byte
data[3] = (Byte)(x_pos >> 8); //3 xpos high byte
data[4] = (Byte)(y_pos & 0xff); //4 ypos low byte
data[5] = (Byte)(y_pos >> 8); //5 ypos high byte
data[6] = (Byte)(width & 0xff); //6 width low byte // 120
data[7] = (Byte)(width >> 8); //7 width high byte
data[8] = (Byte)(height & 0xff); //8 height low byte // 120
data[9] = (Byte)(height >> 8); //9 height high byte
data[10] = (Byte)(isLast ? 1 : 0);
data[11] = (Byte)(pageNumber & 0xff);
data[12] = (Byte)(pageNumber >> 8);
data[13] = (Byte)(payloadLength & 0xff);
data[14] = (Byte)(payloadLength >> 8);
data[15] = 0x00;
}
/// <inheritdoc/>
public Byte[] GetBrightnessMessage(Byte percent) {
if(percent > 100) {
throw new ArgumentOutOfRangeException(nameof(percent));
}
Byte[] buffer =
[
0x03, 0x08, 0x64, 0x23, 0xB8, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xA5, 0x49, 0xCD, 0x02, 0xFE, 0x7F, 0x00, 0x00,
];
buffer[2] = percent;
buffer[3] = 0x23; // 0x23, sometimes 0x27
return buffer;
}
/// <inheritdoc/>
public KeyBitmap AdjustImageSize(KeyBitmap bitmapData, Int32 x_pos, Int32 y_pos) {
if(x_pos >= this.TouchWidth || y_pos >= this.TouchHeight || x_pos < 0 || y_pos < 0) {
throw new ArgumentException("x or y coordinate is bigger than screen or negative");
}
Int32 width = bitmapData.Width;
if(x_pos + bitmapData.Width > this.TouchWidth) {
width = bitmapData.Width - x_pos + bitmapData.Width - this.TouchWidth;
}
/*Int32 height = bitmapData.Height;
if(y_pos + bitmapData.Height > this.TouchHeight) {
height = bitmapData.Height - y_pos + bitmapData.Height - this.TouchHeight;
}*/ //NEEDS TO BE TOUCHSIZE IN HEIGHT!
Int32 height = this.TouchHeight;
if(bitmapData.Width == width && bitmapData.Height == height) {
// if it is already the size we need just return the underlying data
return bitmapData;
}
IKeyBitmapDataAccess keyDataAccess = bitmapData;
using Image<Bgr24> image = keyDataAccess.ToImage();
image.Mutate(x => x.Resize(width, height));
return KeyBitmap.Create.FromImageSharpImage(image);
}
/// <inheritdoc/>
public Byte[] GetLogoMessage() => [0x03, 0x02];
private Byte[] GetNullImage(Int32 width, Int32 height) {
if(this.cachedNullImage is null) {
ReadOnlySpan<Byte> rawNullImg = KeyBitmap.Create.FromBgr24Array(1, 1, [0, 0, 0]).GetScaledVersion(width, height);
this.cachedNullImage = this.EncodeImageToJpg(rawNullImg, width, height);
}
return this.cachedNullImage;
}
private Byte[] EncodeImageToJpg(ReadOnlySpan<Byte> bgr24, Int32 width, Int32 height) {
using Image<Bgr24> image = Image.LoadPixelData<Bgr24>(bgr24, width, height);
image.Mutate(x => x.Rotate(-90));
using MemoryStream memStream = new();
image.SaveAsJpeg(memStream, this.jpgEncoder);
//Byte[] buffer = memStream.ToArray();
//Console.WriteLine(BitConverter.ToString(buffer).Replace("-", "").Substring(0, 100));
//return buffer;
return memStream.ToArray();
}
}
}

View File

@ -0,0 +1,163 @@
using OpenMacroBoard.SDK;
using System;
namespace StreamDeckSharp.Internals {
/// <summary>
/// HID Stream Deck communication driver for the Stream Deck Mini.
/// </summary>
public sealed class HidComDriverStreamDeckMini : IStreamDeckHidComDriver {
private const Int32 ColorChannels = 3;
private static readonly Byte[] BmpHeader = new Byte[]
{
0x42, 0x4d, 0x36, 0x4b, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x50, 0x00,
0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0xc4, 0x0e,
0x00, 0x00, 0xc4, 0x0e, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
private readonly Int32 imgSize;
/// <summary>
/// Initializes a new instance of the <see cref="HidComDriverStreamDeckMini"/> class.
/// </summary>
/// <param name="imgSize">The size of the button images in pixels.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="imgSize"/> is smaller than one.</exception>
public HidComDriverStreamDeckMini(Int32 imgSize) {
if(imgSize < 1) {
throw new ArgumentOutOfRangeException(nameof(imgSize));
}
this.imgSize = imgSize;
}
/// <inheritdoc/>
public Int32 HeaderSizeButton => 16;
/// <inheritdoc/>
public Int32 HeaderSizeFullScreenImage => throw new NotImplementedException();
/// <inheritdoc/>
public Int32 HeaderSizeWindowImage => throw new NotImplementedException();
/// <inheritdoc/>
public Int32 HeaderSizePartialWindowImage => throw new NotImplementedException();
/// <inheritdoc/>
public Int32 ReportSize => 1024;
/// <inheritdoc/>
public Int32 ExpectedFeatureReportLength => 17;
/// <inheritdoc/>
public Int32 ExpectedOutputReportLength => 1024;
/// <inheritdoc/>
public Int32 ExpectedInputReportLength => 17;
/// <inheritdoc/>
public Int32 KeyReportOffset => 1;
/// <inheritdoc/>
public Byte FirmwareVersionFeatureId => 4;
/// <inheritdoc/>
public Byte SerialNumberFeatureId => 3;
/// <inheritdoc/>
public Int32 FirmwareVersionReportSkip => 5;
/// <inheritdoc/>
public Int32 SerialNumberReportSkip => 5;
/// <inheritdoc/>
public Double BytesPerSecondLimit => Double.PositiveInfinity;
/// <inheritdoc/>
public Byte[] GenerateButtonImage(KeyBitmap keyBitmap) {
ReadOnlySpan<Byte> rawData = keyBitmap.GetScaledVersion(this.imgSize, this.imgSize);
Byte[] bmp = new Byte[this.imgSize * this.imgSize * ColorChannels + BmpHeader.Length];
Array.Copy(BmpHeader, 0, bmp, 0, BmpHeader.Length);
if(rawData.Length != 0) {
for(Int32 y = 0; y < this.imgSize; y++) {
for(Int32 x = 0; x < this.imgSize; x++) {
Int32 src = (y * this.imgSize + x) * ColorChannels;
Int32 tar = ((this.imgSize - x - 1) * this.imgSize + y) * ColorChannels + BmpHeader.Length;
bmp[tar + 0] = rawData[src + 0];
bmp[tar + 1] = rawData[src + 1];
bmp[tar + 2] = rawData[src + 2];
}
}
}
return bmp;
}
/// <inheritdoc/>
public Byte[] GenerateFullScreenImage(KeyBitmap keyBitmap) => throw new NotImplementedException();
/// <inheritdoc/>
public Byte[] GenerateWindowImage(KeyBitmap keyBitmap) => throw new NotImplementedException();
/// <inheritdoc/>
public Byte[] GeneratePartialWindowImage(KeyBitmap keyBitmap) => throw new NotImplementedException();
/// <inheritdoc/>
public KeyBitmap AdjustImageSize(KeyBitmap bitmapData, Int32 x_pos, Int32 y_pos) => throw new NotImplementedException();
/// <inheritdoc/>
public Int32 ExtKeyIdToHardwareKeyId(Int32 extKeyId) {
return extKeyId;
}
/// <inheritdoc/>
public Int32 HardwareKeyIdToExtKeyId(Int32 hardwareKeyId) {
return hardwareKeyId;
}
/// <inheritdoc/>
public void PrepareHeaderOutButtonImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Int32 keyId, Boolean isLast) {
data[0] = 2; // Report ID ?
data[1] = 1; // ?
data[2] = (Byte)pageNumber;
data[4] = (Byte)(isLast ? 1 : 0);
data[5] = (Byte)(keyId + 1);
}
public void PrepareHeaderOutFullScreenImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast) => throw new NotImplementedException();
public void PrepareHeaderOutWindowImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast) => throw new NotImplementedException();
public void PrepareHeaderOutPartialWindowImage(Byte[] data, Int32 x_pos, Int32 y_pos, Int32 width, Int32 height, Int32 pageNumber, Int32 payloadLength, Boolean isLast) => throw new NotImplementedException();
/// <inheritdoc/>
public Byte[] GetBrightnessMessage(Byte percent) {
if(percent > 100) {
throw new ArgumentOutOfRangeException(nameof(percent));
}
Byte[] buffer = new Byte[]
{
0x05, 0x55, 0xaa, 0xd1, 0x01, 0x64, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
};
buffer[5] = percent;
return buffer;
}
/// <inheritdoc/>
public Byte[] GetLogoMessage() {
return new Byte[] { 0x0B, 0x63 };
}
}
}

View File

@ -0,0 +1,7 @@
using HidSharp;
namespace StreamDeckSharp.Internals {
internal static class HidDeviceExtensions {
public static UsbHardwareIdAndDriver GetHardwareInformation(this HidDevice hid) => Hardware.GetInternalHardwareInfos(new(hid.VendorID, hid.ProductID));
}
}

View File

@ -0,0 +1,20 @@
using OpenMacroBoard.SDK;
using System;
namespace StreamDeckSharp.Internals {
internal interface IStreamDeckHid : IDisposable {
event EventHandler<IReportsIn> ReportReceived;
event EventHandler<ConnectionEventArgs> ConnectionStateChanged;
Boolean IsConnected {
get;
}
Int32 OutputReportLength {
get;
}
Boolean WriteFeature(Byte[] featureData);
Boolean WriteReport(Byte[] reportData);
Boolean ReadFeatureData(Byte id, out Byte[] data);
}
}

View File

@ -0,0 +1,223 @@
using OpenMacroBoard.SDK;
using System;
namespace StreamDeckSharp.Internals {
/// <summary>
/// Interface that describes the StreamDeck HID communication.
/// </summary>
/// <remarks>
/// <para>
/// Unless there are new stream deck versions you likely don't have to deal with this interface
/// or implementations of it directly as a consumer of the library. Because this interface is
/// low level, some members may not have a good documentation or any at all.
/// </para>
/// <para>Implementations must be thread-safe.</para>
/// </remarks>
public interface IStreamDeckHidComDriver {
/// <summary>
/// Gets the header size for a specific button.
/// </summary>
Int32 HeaderSizeButton {
get;
}
/// <summary>
/// Gets the header size for the complete LCD.
/// </summary>
Int32 HeaderSizeFullScreenImage {
get;
}
/// <summary>
/// Gets the header size for the touchscreen window strip.
/// </summary>
Int32 HeaderSizeWindowImage {
get;
}
/// <summary>
/// Gets the header size for a rectangular region of the touchscreen window.
/// </summary>
Int32 HeaderSizePartialWindowImage {
get;
}
/// <summary>
/// Gets the report size.
/// </summary>
Int32 ReportSize {
get;
}
/// <summary>
/// Gets the feature report length for the device.
/// </summary>
/// <remarks>
/// <para>This is asserted (in debug mode).</para>
/// </remarks>
Int32 ExpectedFeatureReportLength {
get;
}
/// <summary>
/// Gets the output report length for the device.
/// </summary>
/// <remarks>
/// <para>This is asserted (in debug mode).</para>
/// </remarks>
Int32 ExpectedOutputReportLength {
get;
}
/// <summary>
/// Gets the input report length for the device.
/// </summary>
/// <remarks>
/// <para>This is asserted (in debug mode).</para>
/// </remarks>
Int32 ExpectedInputReportLength {
get;
}
/// <summary>
/// Gets the offset of the key information inside the key state report.
/// </summary>
Int32 KeyReportOffset {
get;
}
/// <summary>
/// The ID of the feature that identifies the firmware version.
/// </summary>
Byte FirmwareVersionFeatureId {
get;
}
/// <summary>
/// Number of bytes to skip before the firmware version string starts.
/// </summary>
/// <remarks>
/// <para>For details see property documentation of <see cref="SerialNumberReportSkip"/>.</para>
/// </remarks>
Int32 FirmwareVersionReportSkip {
get;
}
/// <summary>
/// The ID of the feature that identifies the serial number.
/// </summary>
Byte SerialNumberFeatureId {
get;
}
/// <summary>
/// Number of bytes to skip before the serial number string starts.
/// </summary>
/// <remarks>
/// <para>For some reason some string reports have some "weird" data prefixed.
/// I guess they are some binary encoded details or headers - no idea.
/// This property can be tweaked so the resulting string doesn't contain
/// strange Unicode characters.</para>
/// </remarks>
Int32 SerialNumberReportSkip {
get;
}
/// <summary>
/// Limits the USB transfer speed.
/// </summary>
/// <remarks>
/// <para>Some stream decks produce artifacts and glitches when data comes in to fast.
/// I'm not sure if this happens because of this library or because of a bug in
/// the stream deck's firmware but currently the work-around is to limit the transfer rate.
/// This value has to be determined experimentally.</para>
/// </remarks>
Double BytesPerSecondLimit {
get;
}
/// <summary>
/// Generate they payload for a given <paramref name="keyBitmap"/>.
/// </summary>
Byte[] GenerateButtonImage(KeyBitmap keyBitmap);
/// <summary>
/// Generate they payload for a given <paramref name="keyBitmap"/>.
/// </summary>
Byte[] GenerateFullScreenImage(KeyBitmap keyBitmap);
/// <summary>
/// Generate they payload for a given <paramref name="keyBitmap"/> for the Touchstripe.
/// </summary>
Byte[] GenerateWindowImage(KeyBitmap keyBitmap);
/// <summary>
/// Generate they payload for a given <paramref name="keyBitmap"/> for the Touchstripe.
/// </summary>
Byte[] GeneratePartialWindowImage(KeyBitmap keyBitmap);
/// <summary>
/// Adjust Picture Size to fit on Screen
/// </summary>
/// <param name="bitmapData">Picture to check or resized</param>
/// <param name="x_pos">Pixel from Top</param>
/// <param name="y_pos">Pixel from Left</param>
/// <returns></returns>
KeyBitmap AdjustImageSize(KeyBitmap bitmapData, Int32 x_pos, Int32 y_pos);
/// <summary>
/// This is used to convert between keyId conventions
/// </summary>
/// <remarks>
/// <para>The original stream deck has a pretty weird way of enumerating keys.
/// Index 0 starts right top and they are enumerated right to left,
/// and top to bottom. Most developers would expect it to be left-to-right
/// instead of right-to-left, so we change that ;-)</para>
/// </remarks>
Int32 ExtKeyIdToHardwareKeyId(Int32 extKeyId);
/// <summary>
/// This is used to convert between keyId conventions
/// </summary>
Int32 HardwareKeyIdToExtKeyId(Int32 hardwareKeyId);
/// <summary>
/// Before the report is sent to the stream deck (human interface device) this is called to
/// prepare meta information and details in the report header. This depends on the target device
/// and has to be reverse engineered with a USB traffic analyzer.
/// </summary>
void PrepareHeaderOutButtonImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Int32 keyId, Boolean isLast);
/// <summary>
/// Before the report is sent to the stream deck (human interface device) this is called to
/// prepare meta information and details in the report header. This depends on the target device
/// and has to be reverse engineered with a USB traffic analyzer.
/// </summary>
void PrepareHeaderOutFullScreenImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast);
/// <summary>
/// Before the report is sent to the stream deck (human interface device) this is called to
/// prepare meta information and details in the report header. This depends on the target device
/// and has to be reverse engineered with a USB traffic analyzer.
/// </summary>
void PrepareHeaderOutWindowImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast);
/// <summary>
/// Before the report is sent to the stream deck (human interface device) this is called to
/// prepare meta information and details in the report header. This depends on the target device
/// and has to be reverse engineered with a USB traffic analyzer.
/// </summary>
void PrepareHeaderOutPartialWindowImage(Byte[] data, Int32 x_pos, Int32 y_pos, Int32 width, Int32 height, Int32 pageNumber, Int32 payloadLength, Boolean isLast);
/// <summary>
/// Generates a message to set a given brightness.
/// </summary>
Byte[] GetBrightnessMessage(Byte percent);
/// <summary>
/// Generates a message to show the vendor logo.
/// </summary>
Byte[] GetLogoMessage();
}
}

View File

@ -0,0 +1,31 @@
using OpenMacroBoard.SDK;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
namespace StreamDeckSharp.Internals {
internal static class KeyBitmapExtensions {
public static ReadOnlySpan<Byte> GetScaledVersion(this KeyBitmap keyBitmap, Int32 width, Int32 height) {
IKeyBitmapDataAccess keyDataAccess = keyBitmap;
if(keyDataAccess.IsEmpty) {
// default span is of length 0 (the caller has to check this special case)
return default;
}
if(keyBitmap.Width == width && keyBitmap.Height == height) {
// if it is already the size we need just return the underlying data
return keyDataAccess.GetData();
}
using Image<Bgr24> image = keyDataAccess.ToImage();
image.Mutate(x => x.Resize(width, height));
Byte[] scaledPixelData = image.ToBgr24PixelArray();
return new ReadOnlySpan<Byte>(scaledPixelData);
}
}
}

View File

@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
namespace StreamDeckSharp.Internals {
internal static class OutputReportSplitter {
public delegate void PrepareHeaderOutButtonImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Int32 keyId, Boolean isLast);
public delegate void PrepareHeaderOutFullScreenImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast);
public delegate void PrepareHeaderOutWindowImage(Byte[] data, Int32 pageNumber, Int32 payloadLength, Boolean isLast);
public delegate void PrepareHeaderOutPartialWindowImage(Byte[] data, Int32 x_pos, Int32 y_pos, Int32 width, Int32 height, Int32 pageNumber, Int32 payloadLength, Boolean isLast);
public static IEnumerable<Byte[]> ButtonImage(Byte[] data, Byte[] buffer, Int32 bufferLength, Int32 headerSize, Int32 keyId, PrepareHeaderOutButtonImage prepareData) {
Int32 maxPayloadLength = bufferLength - headerSize;
Int32 remainingBytes = data.Length;
Int32 bytesSent = 0;
for(Int32 splitNumber = 0; remainingBytes > 0; splitNumber++) {
Boolean isLast = remainingBytes <= maxPayloadLength;
Int32 bytesToSend = Math.Min(remainingBytes, maxPayloadLength);
Array.Copy(data, bytesSent, buffer, headerSize, bytesToSend);
prepareData(buffer, splitNumber, bytesToSend, keyId, isLast);
//String x = BitConverter.ToString(buffer).Replace("-", "");
yield return buffer;
bytesSent += bytesToSend;
remainingBytes -= bytesToSend;
}
}
public static IEnumerable<Byte[]> FullScreenImage(Byte[] data, Byte[] buffer, Int32 bufferLength, Int32 headerSize, PrepareHeaderOutFullScreenImage prepareData) {
Int32 maxPayloadLength = bufferLength - headerSize;
Int32 remainingBytes = data.Length;
Int32 bytesSent = 0;
for(Int32 splitNumber = 0; remainingBytes > 0; splitNumber++) {
Boolean isLast = remainingBytes <= maxPayloadLength;
Int32 bytesToSend = Math.Min(remainingBytes, maxPayloadLength);
Array.Copy(data, bytesSent, buffer, headerSize, bytesToSend);
prepareData(buffer, splitNumber, bytesToSend, isLast);
yield return buffer;
bytesSent += bytesToSend;
remainingBytes -= bytesToSend;
}
}
public static IEnumerable<Byte[]> WindowImage(Byte[] data, Byte[] buffer, Int32 bufferLength, Int32 headerSize, PrepareHeaderOutWindowImage prepareData) {
Int32 maxPayloadLength = bufferLength - headerSize;
Int32 remainingBytes = data.Length;
Int32 bytesSent = 0;
for(Int32 splitNumber = 0; remainingBytes > 0; splitNumber++) {
Boolean isLast = remainingBytes <= maxPayloadLength;
Int32 bytesToSend = Math.Min(remainingBytes, maxPayloadLength);
Array.Copy(data, bytesSent, buffer, headerSize, bytesToSend);
prepareData(buffer, splitNumber, bytesToSend, isLast);
yield return buffer;
bytesSent += bytesToSend;
remainingBytes -= bytesToSend;
}
}
public static IEnumerable<Byte[]> PartialWindowImage(Byte[] data, Byte[] buffer, Int32 bufferLength, Int32 headerSize, Int32 x_pos, Int32 y_pos, Int32 width, Int32 height, PrepareHeaderOutPartialWindowImage prepareData) {
Int32 maxPayloadLength = bufferLength - headerSize;
Int32 remainingBytes = data.Length;
Int32 bytesSent = 0;
for(Int32 splitNumber = 0; remainingBytes > 0; splitNumber++) {
Boolean isLast = remainingBytes <= maxPayloadLength;
Int32 bytesToSend = Math.Min(remainingBytes, maxPayloadLength);
Array.Clear(buffer);
Array.Copy(data, bytesSent, buffer, headerSize, bytesToSend);
prepareData(buffer, x_pos, y_pos, width, height, splitNumber, bytesToSend, isLast);
//String x = BitConverter.ToString(buffer).Replace("-", "");
yield return buffer;
bytesSent += bytesToSend;
remainingBytes -= bytesToSend;
}
}
}
}

View File

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
namespace StreamDeckSharp.Internals {
interface IReportsIn {
}
public class ButtonPressEvent : EventArgs, IReportsIn {
private const Int32 DATA_OFFSET = 4;
public Byte ButtonsCount {
get;
}
public Dictionary<Int32, Boolean> ButtonStates {
get;
}
public ButtonPressEvent(Byte[] data) {
this.ButtonsCount = data[2];
if(this.ButtonsCount + DATA_OFFSET <= data.Length) {
this.ButtonStates = [];
for(Int32 i = 0; i < this.ButtonsCount; i++) {
this.ButtonStates.Add(i, data[i + DATA_OFFSET] == 0x01);
}
}
}
public override String ToString() => "Button Presed: [" + String.Join(", ", this.ButtonStates) + "]";
}
public class TouchTapEvent : EventArgs, IReportsIn {
public Byte Fingers {
get;
}
public UInt16 XCoord {
get;
}
public UInt16 YCoord {
get;
}
public TouchTapEvent(Byte[] data) {
this.Fingers = data[5];
this.XCoord = (UInt16)BitConverter.ToInt16(data, 6);
this.YCoord = (UInt16)BitConverter.ToInt16(data, 8);
}
public override String ToString() => "Touch Tapped on x: " + this.XCoord + ", y: " + this.YCoord;
}
public class TouchPressEvent : EventArgs, IReportsIn {
public Byte Fingers {
get;
}
public UInt16 XCoord {
get;
}
public UInt16 YCoord {
get;
}
public TouchPressEvent(Byte[] data) {
this.Fingers = data[5];
this.XCoord = (UInt16)BitConverter.ToInt16(data, 6);
this.YCoord = (UInt16)BitConverter.ToInt16(data, 8);
}
public override String ToString() => "Touch Pressed on x: " + this.XCoord + ", y: " + this.YCoord;
}
public class TouchFlickEvent : EventArgs, IReportsIn {
public Byte Fingers {
get;
}
public UInt16 XCoordStart {
get;
}
public UInt16 YCoordStart {
get;
}
public UInt16 XCoordStop {
get;
}
public UInt16 YCoordStop {
get;
}
public TouchFlickEvent(Byte[] data) {
this.Fingers = data[5];
this.XCoordStart = (UInt16)BitConverter.ToInt16(data, 6);
this.YCoordStart = (UInt16)BitConverter.ToInt16(data, 8);
this.XCoordStop = (UInt16)BitConverter.ToInt16(data, 10);
this.YCoordStop = (UInt16)BitConverter.ToInt16(data, 12);
}
public override String ToString() => "Touch Swiped from x: " + this.XCoordStart + ", y: " + this.YCoordStart + " to x: " + this.XCoordStop + ", y: " + this.YCoordStop;
}
public class EncoderPressEvent : EventArgs, IReportsIn {
private const Int32 DATA_OFFSET = 5;
public Byte EncoderCount {
get;
}
public Dictionary<Int32, Boolean> EncoderStates {
get;
}
public EncoderPressEvent(Byte[] data) {
this.EncoderCount = (Byte)(data[2] - 1);
if(this.EncoderCount + DATA_OFFSET <= data.Length) {
this.EncoderStates = [];
for(Int32 i = 0; i < this.EncoderCount; i++) {
this.EncoderStates.Add(i, data[i + DATA_OFFSET] == 0x01);
}
}
}
public override String ToString() => "Encoder Presed: [" + String.Join(", ", this.EncoderStates) + "]";
}
public class EncoderRotateEvent : EventArgs, IReportsIn {
private const Int32 DATA_OFFSET = 5;
public Byte EncoderCount {
get;
}
public Dictionary<Int32, SByte> EncoderStates {
get;
}
public EncoderRotateEvent(Byte[] data) {
this.EncoderCount = (Byte)(data[2] - 1);
if(this.EncoderCount + DATA_OFFSET <= data.Length) {
this.EncoderStates = [];
for(Int32 i = 0; i < this.EncoderCount; i++) {
this.EncoderStates.Add(i, (SByte)data[i + DATA_OFFSET]);
}
}
}
public override String ToString() => "Encoder Turned Steps: [" + String.Join(", ", this.EncoderStates) + "]";
}
}

View File

@ -0,0 +1,298 @@
using HidSharp;
using OpenMacroBoard.SDK;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
namespace StreamDeckSharp.Internals {
internal sealed class StreamDeckHidWrapper : IStreamDeckHid {
private readonly Object hidStreamLock = new();
private readonly String devicePath;
/// <summary>
/// Used to throttle write speed.
/// </summary>
/// <remarks>
/// <para>
/// Based on a hand full of speed measurements, it looks like that (at least)
/// the classical stream deck (hardware revision 1) can't keep up with full USB 2.0 speed.
/// </para>
/// <para>
/// For the other devices this limit is also active but probably not relevant,
/// because in practice the speed is slower, because all other devices use
/// JPEG instead of BMP and the Hid.Write probably also blocks as long as the device is busy.
/// </para>
/// <para>
/// The limit was determined by the following measurements with a classical stream deck:</para>
/// <para>
/// write speed -> time between glitches<br/>
/// 3.90 MiB/s -> 1.7s<br/>
/// 3.68 MiB/s -> 3.7s<br/>
/// 3.60 MiB/s -> 7.6s<br/>
/// </para>
/// <para>
/// Based on the assumption, that the stream deck has a maximum speed at which data is processed,
/// the following formular can be used:
/// </para>
/// <para>
/// Measured speed ............ s<br/>
/// Time between glitches ..... t<br/>
/// Internal speed ............ x (to be calculated)<br/>
/// Hardware buffer size ...... b (will be eliminated when solving for x)<br/>
/// </para>
/// <para>(s - x) * t = b</para>
/// <para>(s1 - x) * t1 = (s2 - x) * t2</para>
/// <para>
/// When solved for x and evaluated with all the measured pairs, the calculated internal speed
/// of the classical stream deck seems to be (almost exactly?) 3.50 MiB/s - A few tests indeed
/// showed that limiting the speed below that value seems to prevent glitches.
/// </para>
/// <para>
/// So long story short we set a limit of 3'200'000 bytes/s (~3.0 MiB/s)
/// for all devices that can't keep up or I haven't had the chance to test on a
/// particular Elgato Device (for example I don't own a StreamDeck Rev2) and for
/// other devices that work as expected we set <see cref="Double.PositiveInfinity"/> (unlimited).
/// </para>
/// </remarks>
private readonly Throttle throttle;
private readonly IStreamDeckHidComDriver hardwareInfo;
private HidStream dStream;
private Byte[] readReportBuffer;
public StreamDeckHidWrapper(HidDevice device, IStreamDeckHidComDriver hardwareInfo) {
if(device is null) {
throw new ArgumentNullException(nameof(device));
}
this.hardwareInfo = hardwareInfo ?? throw new ArgumentNullException(nameof(hardwareInfo));
if(hardwareInfo.BytesPerSecondLimit < Double.PositiveInfinity) {
this.throttle = new() {
BytesPerSecondLimit = hardwareInfo.BytesPerSecondLimit
};
}
this.devicePath = device.DevicePath;
DeviceList.Local.Changed += this.Local_Changed;
this.InitializeDeviceSettings(device);
this.OpenConnection(device);
}
public event EventHandler<ConnectionEventArgs> ConnectionStateChanged;
public event EventHandler<IReportsIn> ReportReceived;
public Int32 OutputReportLength {
get; private set;
}
public Int32 FeatureReportLength {
get; private set;
}
public Boolean IsConnected => this.dStream != null;
public void Dispose() {
this.DisposeConnection();
}
public Boolean ReadFeatureData(Byte id, out Byte[] data) {
data = new Byte[this.FeatureReportLength];
data[0] = id;
HidStream targetStream = this.dStream;
if(targetStream is null) {
return false;
}
try {
lock(this.hidStreamLock) {
this.throttle?.MeasureAndBlock(data.Length);
targetStream.GetFeature(data);
return true;
}
} catch(Exception ex) when(ex is TimeoutException or IOException) {
this.DisposeConnection();
return false;
}
}
public Boolean WriteFeature(Byte[] featureData) {
if(featureData.Length != this.FeatureReportLength) {
var resizedData = new Byte[this.FeatureReportLength];
var minLen = Math.Min(this.FeatureReportLength, featureData.Length);
Array.Copy(featureData, 0, resizedData, 0, minLen);
featureData = resizedData;
}
HidStream targetStream = this.dStream;
if(targetStream is null) {
return false;
}
try {
lock(this.hidStreamLock) {
this.throttle?.MeasureAndBlock(featureData.Length);
targetStream.SetFeature(featureData);
}
return true;
} catch(Exception ex) when(IsConnectionError(ex)) {
this.DisposeConnection();
return false;
}
}
public Boolean WriteReport(Byte[] reportData) {
HidStream targetStream = this.dStream;
if(targetStream is null) {
return false;
}
try {
lock(this.hidStreamLock) {
this.throttle?.MeasureAndBlock(reportData.Length);
targetStream.Write(reportData);
}
return true;
} catch(Exception ex) when(IsConnectionError(ex)) {
this.DisposeConnection();
return false;
}
}
private static Boolean IsConnectionError(Exception ex) {
if(ex is TimeoutException) {
return true;
}
if(ex is IOException) {
return true;
}
if(ex is ObjectDisposedException) {
return true;
}
return false;
}
private void OpenConnection(HidDevice device) {
if(device == null) {
return;
}
if(this.dStream != null) {
return;
}
if(device.TryOpen(out HidStream stream)) {
stream.ReadTimeout = Timeout.Infinite;
this.dStream = stream;
this.BeginWaitRead(stream);
ConnectionStateChanged?.Invoke(this, new ConnectionEventArgs(true));
}
}
private void Local_Changed(Object sender, DeviceListChangedEventArgs e) {
this.RefreshConnection();
}
private void InitializeDeviceSettings(HidDevice device) {
var inputReportLength = device.GetMaxInputReportLength();
this.OutputReportLength = device.GetMaxOutputReportLength();
this.FeatureReportLength = device.GetMaxFeatureReportLength();
Debug.Assert(
this.OutputReportLength == this.hardwareInfo.ExpectedOutputReportLength,
$"Output report length unexpected. Found: {this.OutputReportLength}. Expected: {this.hardwareInfo.ExpectedOutputReportLength}"
);
Debug.Assert(
this.FeatureReportLength == this.hardwareInfo.ExpectedFeatureReportLength,
$"Feature report length unexpected. Found: {this.FeatureReportLength}. Expected: {this.hardwareInfo.ExpectedFeatureReportLength}"
);
Debug.Assert(
inputReportLength == this.hardwareInfo.ExpectedInputReportLength,
$"Input report length unexpected. Found: {inputReportLength}. Expected: {this.hardwareInfo.ExpectedInputReportLength}"
);
this.readReportBuffer = new Byte[this.OutputReportLength];
}
private void RefreshConnection() {
HidDevice device = DeviceList.Local
.GetHidDevices()
.FirstOrDefault(d => d.DevicePath == this.devicePath);
var deviceFound = device != null;
var deviceActive = this.dStream != null;
if(deviceFound == deviceActive) {
return;
}
if(!deviceFound) {
this.DisposeConnection();
} else {
this.OpenConnection(device);
}
}
private void DisposeConnection() {
HidStream dStreamRefCopy = this.dStream;
this.dStream = null;
if(dStreamRefCopy is null) {
return;
}
dStreamRefCopy.Dispose();
ConnectionStateChanged?.Invoke(this, new ConnectionEventArgs(false));
}
private void BeginWaitRead(HidStream stream) {
stream.BeginRead(this.readReportBuffer, 0, this.readReportBuffer.Length, new AsyncCallback(this.ReadReportCallback), stream);
}
private void ReadReportCallback(IAsyncResult ar) {
HidStream stream = (HidStream)ar.AsyncState;
try {
if(this.dStream == null) {
// connection already disposed
return;
}
Int32 res = stream.EndRead(ar);
Byte[] data = new Byte[res];
Array.Copy(this.readReportBuffer, 0, data, 0, res);
if(res > 4 && data[0] == 0x01 && data[1] == 0x00) {
this.ReportReceived?.Invoke(this, new ButtonPressEvent(data));
} else if(res > 8 && data[0] == 0x01 && data[1] == 0x02 && data[4] == 0x01) {
this.ReportReceived?.Invoke(this, new TouchTapEvent(data));
} else if(res > 8 && data[0] == 0x01 && data[1] == 0x02 && data[4] == 0x02) {
this.ReportReceived?.Invoke(this, new TouchPressEvent(data));
} else if(res > 12 && data[0] == 0x01 && data[1] == 0x02 && data[4] == 0x03) {
this.ReportReceived?.Invoke(this, new TouchFlickEvent(data));
} else if(res > 5 && data[0] == 0x01 && data[1] == 0x03 && data[4] == 0x00) {
this.ReportReceived?.Invoke(this, new EncoderPressEvent(data));
} else if(res > 5 && data[0] == 0x01 && data[1] == 0x03 && data[4] == 0x01) {
this.ReportReceived?.Invoke(this, new EncoderRotateEvent(data));
}
} catch(Exception ex) when(IsConnectionError(ex)) {
this.DisposeConnection();
return;
}
this.BeginWaitRead(stream);
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Diagnostics;
using System.Threading;
#pragma warning disable AV1710 // Member name includes the name of its containing type
namespace StreamDeckSharp.Internals
{
internal class Throttle
{
private readonly Stopwatch stopwatch = Stopwatch.StartNew();
private long sumBytesInWindow = 0;
private int sleepCount = 0;
public double BytesPerSecondLimit { get; set; } = double.PositiveInfinity;
public int ByteCountBeforeThrottle { get; set; } = 16_000;
public void MeasureAndBlock(int bytes)
{
this.sumBytesInWindow += bytes;
var elapsedSeconds = this.stopwatch.Elapsed.TotalSeconds;
var estimatedSeconds = this.sumBytesInWindow / this.BytesPerSecondLimit;
if (this.sumBytesInWindow > this.ByteCountBeforeThrottle && elapsedSeconds < estimatedSeconds)
{
var delta = Math.Max(1, (int)((estimatedSeconds - elapsedSeconds) * 1000));
Thread.Sleep(delta);
this.sleepCount++;
}
if (elapsedSeconds >= 1)
{
if (this.sleepCount > 1)
{
Debug.WriteLine($"[Throttle] {this.sumBytesInWindow / elapsedSeconds}");
}
this.stopwatch.Restart();
this.sumBytesInWindow = 0;
this.sleepCount = 0;
}
}
}
}

View File

@ -0,0 +1,28 @@
using OpenMacroBoard.SDK;
using System.Collections.Generic;
#pragma warning disable AV1000 // Type name contains the word 'and', which suggests it has multiple purposes
namespace StreamDeckSharp.Internals
{
internal sealed class UsbHardwareIdAndDriver : IUsbHidHardware
{
public UsbHardwareIdAndDriver(
IReadOnlyList<UsbVendorProductPair> usbIds,
string deviceName,
GridKeyLayout keys,
IStreamDeckHidComDriver driver
)
{
this.UsbIds = usbIds;
this.DeviceName = deviceName;
this.Keys = keys;
this.Driver = driver;
}
public IReadOnlyList<UsbVendorProductPair> UsbIds { get; }
public string DeviceName { get; }
public GridKeyLayout Keys { get; }
public IStreamDeckHidComDriver Driver { get; }
}
}

View File

@ -0,0 +1,67 @@
using HidSharp;
using OpenMacroBoard.SDK;
using StreamDeckSharp.Exceptions;
using StreamDeckSharp.Internals;
using System.Collections.Generic;
using System.Linq;
using System;
namespace StreamDeckSharp {
/// <summary>
/// This is a factory class to create IStreamDeck References
/// </summary>
public static class StreamDeck {
/// <summary>
/// Enumerates connected Stream Decks and returns the first one.
/// </summary>
/// <exception cref="StreamDeckNotFoundException">Thrown if no Stream Deck is found</exception>
public static IMacroBoard OpenDevice(params IUsbHidHardware[] hardware) {
return OpenDevice(true, hardware);
}
/// <summary>
/// Enumerates connected Stream Decks and returns the first one.
/// </summary>
/// <exception cref="StreamDeckNotFoundException">Thrown if no Stream Deck is found</exception>
public static IMacroBoard OpenDevice(Boolean useWriteCache, params IUsbHidHardware[] hardware) {
StreamDeckDeviceReference dev = EnumerateDevices(hardware).FirstOrDefault() ?? throw new StreamDeckNotFoundException();
return dev.Open();
}
/// <summary>
/// Enumerates connected Stream Decks and returns the first one.
/// </summary>
/// <exception cref="StreamDeckNotFoundException">Thrown if no Stream Deck is found</exception>
public static IMacroBoard OpenDevice(String devicePath) {
return OpenDevice(devicePath, true);
}
/// <summary>
/// Get the Stream Deck with a given <paramref name="devicePath"/>.
/// </summary>
/// <exception cref="StreamDeckNotFoundException">Thrown if no Stream Deck is found</exception>
public static IMacroBoard OpenDevice(String devicePath, Boolean useWriteCache) {
HidDevice dev = DeviceList.Local.GetHidDevices().First(d => d.DevicePath == devicePath);
return FromHid(dev ?? throw new StreamDeckNotFoundException(), useWriteCache);
}
/// <summary>
/// Enumerate Elgato Stream Deck Devices that match a given type.
/// </summary>
/// <param name="hardware">If no types or null is passed as argument, all known types are found</param>
public static IEnumerable<StreamDeckDeviceReference> EnumerateDevices(params IUsbHidHardware[] hardware) {
return DeviceList.Local.GetStreamDecks(hardware);
}
internal static IMacroBoard FromHid(HidDevice device, Boolean cached) {
UsbHardwareIdAndDriver hwInfo = device.GetHardwareInformation();
StreamDeckHidWrapper hidWrapper = new StreamDeckHidWrapper(device, hwInfo.Driver);
if(cached) {
return new CachedHidClient(hidWrapper, hwInfo.Keys, hwInfo.Driver);
}
return new BasicHidClient(hidWrapper, hwInfo.Keys, hwInfo.Driver);
}
}
}

View File

@ -0,0 +1,83 @@
using OpenMacroBoard.SDK;
using System;
namespace StreamDeckSharp
{
/// <summary>
/// A device reference pointing to a stream deck.
/// </summary>
public sealed class StreamDeckDeviceReference : IDeviceReference
{
internal StreamDeckDeviceReference(
String devicePath,
String deviceName,
GridKeyLayout keyLayout
)
{
this.DevicePath = devicePath;
this.DeviceName = deviceName;
this.Keys = keyLayout;
}
/// <summary>
/// Gets the OSes unique identifier for human interface device.
/// </summary>
public String DevicePath { get; }
/// <inheritdoc/>
public String DeviceName { get; set; }
/// <inheritdoc/>
public IKeyLayout Keys { get; }
/// <inheritdoc/>
public override String ToString()
{
return this.DeviceName;
}
/// <inheritdoc/>
public IMacroBoard Open(Boolean useWriteCache)
{
return StreamDeck.OpenDevice(this.DevicePath, useWriteCache);
}
/// <inheritdoc/>
public IMacroBoard Open()
{
return StreamDeck.OpenDevice(this.DevicePath);
}
/// <inheritdoc/>
public override Int32 GetHashCode()
{
return this.DevicePath.GetHashCode();
}
/// <inheritdoc/>
public override Boolean Equals(Object obj)
{
if (obj is null)
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj is not StreamDeckDeviceReference other)
{
return false;
}
if (other.DevicePath != this.DevicePath)
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,143 @@
using System;
using HidSharp;
using OpenMacroBoard.SDK;
using StreamDeckSharp.Internals;
using System.Collections.Generic;
using System.Linq;
namespace StreamDeckSharp {
/// <summary>
/// A listener for stream deck devices.
/// </summary>
public sealed class StreamDeckListener :
IDisposable,
IObservable<DeviceStateReport> {
private readonly Object sync = new();
private readonly List<DeviceState> knownDevices = new();
private readonly List<Subscription> subscriptions = new();
private readonly Dictionary<String, StreamDeckDeviceReference> knownDeviceLookup = new();
private Boolean disposed = true;
/// <summary>
/// Initializes a new instance of the <see cref="StreamDeckListener"/> class.
/// </summary>
public StreamDeckListener() {
// register event handler before we load the entire list
// so we don't miss stream decks connecting between the calls.
DeviceList.Local.Changed += this.DeviceListChanged;
// initial force load
this.ProcessDelta();
}
/// <inheritdoc />
public IDisposable Subscribe(IObserver<DeviceStateReport> observer) {
Subscription subscription = new Subscription(this, observer);
this.subscriptions.Add(subscription);
subscription.SendUpdates();
return subscription;
}
/// <inheritdoc />
public void Dispose() {
if(this.disposed) {
return;
}
this.disposed = true;
DeviceList.Local.Changed -= this.DeviceListChanged;
}
private void ProcessDelta() {
lock(this.sync) {
// because the HidDevice event doesn't tell us what changed
// we calculate the difference ourselves.
Dictionary<String, StreamDeckDeviceReference> currentDevices = DeviceList.Local
.GetStreamDecks()
.ToDictionary(s => s.DevicePath, s => s);
// update connection states of known devices
foreach(DeviceState knownDevice in this.knownDevices) {
knownDevice.Connected = currentDevices.ContainsKey(knownDevice.DeviceReference.DevicePath);
}
// add new devices
foreach(KeyValuePair<String, StreamDeckDeviceReference> currentDevice in currentDevices) {
if(this.knownDeviceLookup.ContainsKey(currentDevice.Key)) {
// skip because this one is already known
continue;
}
this.knownDeviceLookup.Add(currentDevice.Key, currentDevice.Value);
this.knownDevices.Add(new DeviceState(currentDevice.Value, true));
}
// send updates to all subscribers
foreach(Subscription subscription in this.subscriptions) {
subscription.SendUpdates();
}
}
}
private void DeviceListChanged(Object sender, DeviceListChangedEventArgs e) {
this.ProcessDelta();
}
private sealed class DeviceState {
public DeviceState(StreamDeckDeviceReference deviceReference, Boolean connected) {
this.DeviceReference = deviceReference ?? throw new ArgumentNullException(nameof(deviceReference));
this.Connected = connected;
}
public StreamDeckDeviceReference DeviceReference {
get;
}
public Boolean Connected {
get; set;
}
}
private sealed class Subscription : IDisposable {
private readonly StreamDeckListener parent;
private readonly IObserver<DeviceStateReport> observer;
/// <summary>
/// Contains the state the subscriber knows about.
/// This is used to calculate new updates.
/// </summary>
private readonly List<Boolean> subscriberState = new();
public Subscription(StreamDeckListener parent, IObserver<DeviceStateReport> observer) {
this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
this.observer = observer ?? throw new ArgumentNullException(nameof(observer));
}
public void SendUpdates() {
// send updates for existing devices
for(Int32 i = 0; i < this.subscriberState.Count; i++) {
DeviceState device = this.parent.knownDevices[i];
if(device.Connected != this.subscriberState[i]) {
// report new connection state
this.observer.OnNext(new DeviceStateReport(device.DeviceReference, device.Connected, false));
this.subscriberState[i] = device.Connected;
}
}
// add and send updates for new (to this subscriber) devices.
for(Int32 i = this.subscriberState.Count; i < this.parent.knownDevices.Count; i++) {
DeviceState device = this.parent.knownDevices[i];
this.subscriberState.Add(device.Connected);
this.observer.OnNext(new DeviceStateReport(device.DeviceReference, device.Connected, true));
}
}
public void Dispose() {
lock(this.parent.sync) {
this.parent.subscriptions.Remove(this);
}
}
}
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\ImageSharp\ImageSharp\ImageSharp.csproj" />
<ProjectReference Include="..\OpenMacroBoard.SDK\OpenMacroBoard.SDK.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HidSharp" Version="2.6.4" />
<None Include="icon.png" Pack="true" PackagePath="" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,32 @@
<?xml version="1.0"?>
<package>
<metadata>
<id>StreamDeckSharp</id>
<version>$version$</version>
<title>StreamDeckSharp</title>
<authors>Christian Franzl</authors>
<license type="expression">MIT</license>
<repository type="git" url="https://github.com/OpenMacroBoard/StreamDeckSharp" />
<projectUrl>https://github.com/OpenMacroBoard/StreamDeckSharp</projectUrl>
<iconUrl>https://raw.githubusercontent.com/OpenMacroBoard/StreamDeckSharp/master/doc/icon64.png</iconUrl>
<icon>icon.png</icon>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A simple .NET interface for the StreamDeck HID</description>
<tags>streamdeck elgato stream deck open macro board openmacroboard streamdeckmini</tags>
<dependencies>
<group targetFramework="netstandard2.0">
<dependency id="OpenMacroBoard.SDK" version="[5.0, 6.0)" exclude="Build,Analyzers" />
<dependency id="System.Drawing.Common" version="6.0.0" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
<files>
<file src="icon.png" target="" />
<file src="bin\$configuration$\netstandard2.0\StreamDeckSharp.xml" target="lib\netstandard2.0\" />
<file src="bin\$configuration$\netstandard2.0\StreamDeckSharp.dll" target="lib\netstandard2.0\" />
<file src="bin\$configuration$\netstandard2.0\StreamDeckSharp.pdb" target="lib\netstandard2.0\" />
<file src="bin\$configuration$\netstandard2.0\HidSharp.xml" target="lib\netstandard2.0\" />
<file src="bin\$configuration$\netstandard2.0\HidSharp.dll" target="lib\netstandard2.0\" />
<file src="bin\$configuration$\netstandard2.0\HidSharp.pdb" target="lib\netstandard2.0\" />
</files>
</package>

View File

@ -0,0 +1,31 @@
using System;
namespace StreamDeckSharp
{
/// <summary>
/// A collection of Stream Deck USB related constants.
/// </summary>
public static class UsbConstants
{
/// <summary>
/// Helper function to create a <see cref="UsbVendorProductPair"/> for Elgato devices
/// (with the vendor id <see cref="VendorIds.ElgatoSystemsGmbH"/>).
/// </summary>
/// <param name="productId">USB product id.</param>
public static UsbVendorProductPair ElgatoUsbId(int productId)
{
return new UsbVendorProductPair(VendorIds.ElgatoSystemsGmbH, productId);
}
/// <summary>
/// Known (Stream Deck related) USB Vendor IDs.
/// </summary>
public static class VendorIds
{
/// <summary>
/// The USB Vendor ID for Elgato Systems GmbH.
/// </summary>
public const Int32 ElgatoSystemsGmbH = 0x0fd9;
}
}
}

View File

@ -0,0 +1,80 @@
using System;
namespace StreamDeckSharp
{
/// <summary>
/// Fully quallified USB product identifier. Includes the USB Vendor ID and the USB Product ID.
/// </summary>
public readonly struct UsbVendorProductPair : IEquatable<UsbVendorProductPair>
{
/// <summary>
/// Initializes a new instance of the <see cref="UsbVendorProductPair"/> struct.
/// </summary>
public UsbVendorProductPair(Int32 vendorId, Int32 productId)
{
this.UsbVendorId = vendorId;
this.UsbProductId = productId;
}
/// <summary>
/// USB vendor id
/// </summary>
public Int32 UsbVendorId { get; }
/// <summary>
/// USB product id
/// </summary>
public Int32 UsbProductId { get; }
/// <summary>
/// The == operator. Calls <see cref="Equals(UsbVendorProductPair, UsbVendorProductPair)"/> internally.
/// </summary>
public static Boolean operator ==(UsbVendorProductPair a, UsbVendorProductPair b)
{
return Equals(a, b);
}
/// <summary>
/// The == operator. Calls <see cref="Equals(UsbVendorProductPair, UsbVendorProductPair)"/> internally
/// and inverts the result.
/// </summary>
public static Boolean operator !=(UsbVendorProductPair a, UsbVendorProductPair b)
{
return !Equals(a, b);
}
/// <summary>
/// Indicates whether the two givel objects is equal.
/// </summary>
/// <param name="a">First object.</param>
/// <param name="b">Second object.</param>
/// <returns>true if the two objects are equal; otherwise, false.</returns>
public static Boolean Equals(UsbVendorProductPair a, UsbVendorProductPair b)
{
return a.UsbVendorId == b.UsbVendorId && a.UsbProductId == b.UsbProductId;
}
/// <inheritdoc/>
public Boolean Equals(UsbVendorProductPair other)
{
return Equals(this, other);
}
/// <inheritdoc/>
public override Boolean Equals(Object obj)
{
if (obj is not UsbVendorProductPair other)
{
return false;
}
return Equals(this, other);
}
/// <inheritdoc/>
public override Int32 GetHashCode()
{
return this.UsbVendorId.GetHashCode() ^ this.UsbProductId.GetHashCode();
}
}
}

BIN
StreamDeckSharp/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB