first commit
This commit is contained in:
commit
308ba282fb
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.vs
|
||||
OpenMacroBoard.SDK/bin
|
||||
OpenMacroBoard.SDK/obj
|
||||
StreamDeckSharp/bin
|
||||
StreamDeckSharp/obj
|
||||
118
OpenMacroBoard.SDK/ButtonPressEffectAdapter.cs
Normal file
118
OpenMacroBoard.SDK/ButtonPressEffectAdapter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
OpenMacroBoard.SDK/ButtonPressEffectConfig.cs
Normal file
35
OpenMacroBoard.SDK/ButtonPressEffectConfig.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
50
OpenMacroBoard.SDK/ConditionalDisposable{T}.cs
Normal file
50
OpenMacroBoard.SDK/ConditionalDisposable{T}.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
OpenMacroBoard.SDK/ConnectionEventArgs.cs
Normal file
23
OpenMacroBoard.SDK/ConnectionEventArgs.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
32
OpenMacroBoard.SDK/DeviceConnectionChangedEventArgs.cs
Normal file
32
OpenMacroBoard.SDK/DeviceConnectionChangedEventArgs.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
19
OpenMacroBoard.SDK/DeviceContext.cs
Normal file
19
OpenMacroBoard.SDK/DeviceContext.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
125
OpenMacroBoard.SDK/DeviceContextExtensions.cs
Normal file
125
OpenMacroBoard.SDK/DeviceContextExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
131
OpenMacroBoard.SDK/DeviceListenerBase.cs
Normal file
131
OpenMacroBoard.SDK/DeviceListenerBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
OpenMacroBoard.SDK/DeviceStateReport.cs
Normal file
38
OpenMacroBoard.SDK/DeviceStateReport.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
54
OpenMacroBoard.SDK/DisconnectReplayAdapter.cs
Normal file
54
OpenMacroBoard.SDK/DisconnectReplayAdapter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
OpenMacroBoard.SDK/DrawFullScreenExtensions.cs
Normal file
123
OpenMacroBoard.SDK/DrawFullScreenExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
749
OpenMacroBoard.SDK/GraphicPrimitives/OmbColor.NamedColors.cs
Normal file
749
OpenMacroBoard.SDK/GraphicPrimitives/OmbColor.NamedColors.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
99
OpenMacroBoard.SDK/GraphicPrimitives/OmbColor.cs
Normal file
99
OpenMacroBoard.SDK/GraphicPrimitives/OmbColor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
139
OpenMacroBoard.SDK/GraphicPrimitives/OmbPoint.cs
Normal file
139
OpenMacroBoard.SDK/GraphicPrimitives/OmbPoint.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
199
OpenMacroBoard.SDK/GraphicPrimitives/OmbRectangle.cs
Normal file
199
OpenMacroBoard.SDK/GraphicPrimitives/OmbRectangle.cs
Normal 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} ]";
|
||||
}
|
||||
}
|
||||
}
|
||||
156
OpenMacroBoard.SDK/GraphicPrimitives/OmbSize.cs
Normal file
156
OpenMacroBoard.SDK/GraphicPrimitives/OmbSize.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
OpenMacroBoard.SDK/GraphicPrimitives/README.md
Normal file
5
OpenMacroBoard.SDK/GraphicPrimitives/README.md
Normal 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
|
||||
96
OpenMacroBoard.SDK/GridKeyLayout.cs
Normal file
96
OpenMacroBoard.SDK/GridKeyLayout.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
73
OpenMacroBoard.SDK/IDeviceContext.cs
Normal file
73
OpenMacroBoard.SDK/IDeviceContext.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
29
OpenMacroBoard.SDK/IDeviceReference.cs
Normal file
29
OpenMacroBoard.SDK/IDeviceReference.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
30
OpenMacroBoard.SDK/IKeyBitmapDataAccess.cs
Normal file
30
OpenMacroBoard.SDK/IKeyBitmapDataAccess.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
14
OpenMacroBoard.SDK/IKeyBitmapFactory.cs
Normal file
14
OpenMacroBoard.SDK/IKeyBitmapFactory.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
61
OpenMacroBoard.SDK/IKeyLayout.cs
Normal file
61
OpenMacroBoard.SDK/IKeyLayout.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
15
OpenMacroBoard.SDK/IKnownDevice.cs
Normal file
15
OpenMacroBoard.SDK/IKnownDevice.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
140
OpenMacroBoard.SDK/IMacroBoard.cs
Normal file
140
OpenMacroBoard.SDK/IMacroBoard.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
88
OpenMacroBoard.SDK/ImageSharpExtensions.cs
Normal file
88
OpenMacroBoard.SDK/ImageSharpExtensions.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
38
OpenMacroBoard.SDK/Internals/ConstrainedContext.cs
Normal file
38
OpenMacroBoard.SDK/Internals/ConstrainedContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
OpenMacroBoard.SDK/Internals/DeviceContextInternal.cs
Normal file
56
OpenMacroBoard.SDK/Internals/DeviceContextInternal.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
OpenMacroBoard.SDK/Internals/DeviceStateObserver.cs
Normal file
34
OpenMacroBoard.SDK/Internals/DeviceStateObserver.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
OpenMacroBoard.SDK/Internals/KnownDeviceInternal.cs
Normal file
38
OpenMacroBoard.SDK/Internals/KnownDeviceInternal.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
OpenMacroBoard.SDK/Internals/MergedDeviceListener.cs
Normal file
26
OpenMacroBoard.SDK/Internals/MergedDeviceListener.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
139
OpenMacroBoard.SDK/KeyBitmap.cs
Normal file
139
OpenMacroBoard.SDK/KeyBitmap.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
249
OpenMacroBoard.SDK/KeyBitmapBasicFactoryExtensions.cs
Normal file
249
OpenMacroBoard.SDK/KeyBitmapBasicFactoryExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
OpenMacroBoard.SDK/KeyBitmapDataAccessExtensions.cs
Normal file
24
OpenMacroBoard.SDK/KeyBitmapDataAccessExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
107
OpenMacroBoard.SDK/KeyEventArgs.cs
Normal file
107
OpenMacroBoard.SDK/KeyEventArgs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
33
OpenMacroBoard.SDK/KeyLayoutAreaHelperExtensions.cs
Normal file
33
OpenMacroBoard.SDK/KeyLayoutAreaHelperExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
144
OpenMacroBoard.SDK/MacroBoardAdapter.cs
Normal file
144
OpenMacroBoard.SDK/MacroBoardAdapter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
OpenMacroBoard.SDK/MacroBoardFeatureExtensions.cs
Normal file
47
OpenMacroBoard.SDK/MacroBoardFeatureExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
OpenMacroBoard.SDK/OpenMacroBoard.SDK.csproj
Normal file
28
OpenMacroBoard.SDK/OpenMacroBoard.SDK.csproj
Normal 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>
|
||||
49
OpenMacroBoard.SDK/SetKeyExtensions.cs
Normal file
49
OpenMacroBoard.SDK/SetKeyExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
OpenMacroBoard.SDK/Utils/LinqExtensions.cs
Normal file
42
OpenMacroBoard.SDK/Utils/LinqExtensions.cs
Normal 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
BIN
OpenMacroBoard.SDK/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
40
StreamDeckSharp.sln
Normal file
40
StreamDeckSharp.sln
Normal 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
|
||||
3
StreamDeckSharp/AssemblyInfo.cs
Normal file
3
StreamDeckSharp/AssemblyInfo.cs
Normal file
@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StreamDeckSharp.Tests")]
|
||||
37
StreamDeckSharp/Exceptions/StreamDeckException.cs
Normal file
37
StreamDeckSharp/Exceptions/StreamDeckException.cs
Normal 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
39
StreamDeckSharp/Exceptions/StreamDeckNotFoundException.cs
Normal file
39
StreamDeckSharp/Exceptions/StreamDeckNotFoundException.cs
Normal 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
215
StreamDeckSharp/Hardware.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
StreamDeckSharp/IHardware.cs
Normal file
22
StreamDeckSharp/IHardware.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
StreamDeckSharp/IUsbHidHardware.cs
Normal file
15
StreamDeckSharp/IUsbHidHardware.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
219
StreamDeckSharp/Internals/BasicHidClient.cs
Normal file
219
StreamDeckSharp/Internals/BasicHidClient.cs
Normal 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());
|
||||
|
||||
}
|
||||
}
|
||||
113
StreamDeckSharp/Internals/CachedHidClient.cs
Normal file
113
StreamDeckSharp/Internals/CachedHidClient.cs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
StreamDeckSharp/Internals/ConcurrentBufferedQueue.cs
Normal file
133
StreamDeckSharp/Internals/ConcurrentBufferedQueue.cs
Normal 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>));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
StreamDeckSharp/Internals/DeviceListExtensions.cs
Normal file
62
StreamDeckSharp/Internals/DeviceListExtensions.cs
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
153
StreamDeckSharp/Internals/HidComDriverStreamDeck.cs
Normal file
153
StreamDeckSharp/Internals/HidComDriverStreamDeck.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
263
StreamDeckSharp/Internals/HidComDriverStreamDeckJpeg.cs
Normal file
263
StreamDeckSharp/Internals/HidComDriverStreamDeckJpeg.cs
Normal 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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
163
StreamDeckSharp/Internals/HidComDriverStreamDeckMini.cs
Normal file
163
StreamDeckSharp/Internals/HidComDriverStreamDeckMini.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
7
StreamDeckSharp/Internals/HidDeviceExtensions.cs
Normal file
7
StreamDeckSharp/Internals/HidDeviceExtensions.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
20
StreamDeckSharp/Internals/IStreamDeckHid.cs
Normal file
20
StreamDeckSharp/Internals/IStreamDeckHid.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
223
StreamDeckSharp/Internals/IStreamDeckHidComDriver.cs
Normal file
223
StreamDeckSharp/Internals/IStreamDeckHidComDriver.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
31
StreamDeckSharp/Internals/KeyBitmapExtensions.cs
Normal file
31
StreamDeckSharp/Internals/KeyBitmapExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
StreamDeckSharp/Internals/OutputReportSplitter.cs
Normal file
93
StreamDeckSharp/Internals/OutputReportSplitter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
StreamDeckSharp/Internals/ReportReceivedEventArgs.cs
Normal file
132
StreamDeckSharp/Internals/ReportReceivedEventArgs.cs
Normal 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) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
298
StreamDeckSharp/Internals/StreamDeckHidWrapper.cs
Normal file
298
StreamDeckSharp/Internals/StreamDeckHidWrapper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
StreamDeckSharp/Internals/Throttle.cs
Normal file
45
StreamDeckSharp/Internals/Throttle.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
StreamDeckSharp/Internals/UsbHardwareIdAndDriver.cs
Normal file
28
StreamDeckSharp/Internals/UsbHardwareIdAndDriver.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
67
StreamDeckSharp/StreamDeck.cs
Normal file
67
StreamDeckSharp/StreamDeck.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
StreamDeckSharp/StreamDeckDeviceReference.cs
Normal file
83
StreamDeckSharp/StreamDeckDeviceReference.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
143
StreamDeckSharp/StreamDeckListener.cs
Normal file
143
StreamDeckSharp/StreamDeckListener.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
StreamDeckSharp/StreamDeckSharp.csproj
Normal file
17
StreamDeckSharp/StreamDeckSharp.csproj
Normal 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>
|
||||
32
StreamDeckSharp/StreamDeckSharp.nuspec
Normal file
32
StreamDeckSharp/StreamDeckSharp.nuspec
Normal 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>
|
||||
31
StreamDeckSharp/UsbConstants.cs
Normal file
31
StreamDeckSharp/UsbConstants.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
StreamDeckSharp/UsbVendorProductPair.cs
Normal file
80
StreamDeckSharp/UsbVendorProductPair.cs
Normal 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
BIN
StreamDeckSharp/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Loading…
Reference in New Issue
Block a user