Skip to content

Commit

Permalink
Update X1 Support for LED (Valkirie#293)
Browse files Browse the repository at this point in the history
* Add X1 LED Support

* add .gitignore entries to skip files for jetbrains rider

* add comments for SerialQueueTasks.cs

* update x1 implementation

1. move LED code to X1 class
2. removed class OneXBaseDevice
3. added a skipping logic to SerialUSBIMU to avoid conflict with X1 COM3

* Improve COM ports in use handling

* Fix, allow for setting SerialPortNamesInUse by other classes

---------

Co-authored-by: CasperH2O <[email protected]>
  • Loading branch information
joshuatam and CasperH2O authored Jun 7, 2024
1 parent dfa2f1b commit 9f7ab8c
Show file tree
Hide file tree
Showing 4 changed files with 344 additions and 1 deletion.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ obj/
install/
.vs/
*.user
*Backup.csproj
*Backup.csproj
*tmp.csproj
.idea/
164 changes: 164 additions & 0 deletions HandheldCompanion/Devices/OneXPlayer/OneXPlayerX1.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
using HandheldCompanion.Inputs;
using HandheldCompanion.Managers;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using System.Windows.Media;
using WindowsInput.Events;
using static HandheldCompanion.Utils.DeviceUtils;
using HandheldCompanion.Misc.Threading.Tasks;
using HandheldCompanion.Sensors;

namespace HandheldCompanion.Devices;

public class OneXPlayerX1 : IDevice
{
private SerialPort? _serialPort; // COM3 SerialPort for Device control of OneXPlayer

// Enable COM Port for LED Control
public bool EnableSerialPort = true;
public string SerialPortName = "COM3";
public int SerialPortBaudRate = 115200;
public Parity SerialPortParity = Parity.Even;
public int SerialPortDataBits = 8;
public StopBits SerialPortStopBits = StopBits.Two;

public int TaskDelay = 200;

private readonly SerialQueue _queue = new SerialQueue();

public OneXPlayerX1()
{
// device specific settings
Expand All @@ -32,6 +53,11 @@ public OneXPlayerX1()

// device specific capacities
Capabilities = DeviceCapabilities.FanControl;
Capabilities |= DeviceCapabilities.DynamicLighting;
Capabilities |= DeviceCapabilities.DynamicLightingBrightness;
Capabilities |= DeviceCapabilities.DynamicLightingSecondLEDColor;
DynamicLightingCapabilities |= LEDLevel.SolidColor;
DynamicLightingCapabilities |= LEDLevel.Rainbow;

ECDetails = new ECDetails
{
Expand Down Expand Up @@ -67,6 +93,23 @@ public override bool Open()
if (!success)
return false;


if (EnableSerialPort)
{
// Add the serial port name to be excluded for other instances
SerialUSBIMU.SerialPortNamesInUse.Add(SerialPortName);

// Initialize and open the serial port if it has not been initialized yet
if (_serialPort is null)
{
_serialPort = new SerialPort(SerialPortName, SerialPortBaudRate, SerialPortParity, SerialPortDataBits,
SerialPortStopBits);
_serialPort.Open();

LogManager.LogInformation("Enabled Serial Port Control: {0}", _serialPort.PortName);
}
}

// allow OneX button to pass key inputs
LogManager.LogInformation("Unlocked {0} OEM button", ButtonFlags.OEM1);

Expand All @@ -77,8 +120,129 @@ public override bool Open()

public override void Close()
{
if (_serialPort is not null && _serialPort.IsOpen)
{
_serialPort.Close();
}

LogManager.LogInformation("Locked {0} OEM button", ButtonFlags.OEM1);
ECRamDirectWrite(0x4EB, ECDetails, 0x00);
base.Close();
}

public override bool SetLedStatus(bool enable)
{
// Turn On/Off X1 Back LED
byte[] prefix = { 0xFD, 0x3F };
byte[] positionL = { 0x03 };
byte[] positionR = { 0x04 };
byte[] LEDOptionOn = { 0xFD, 0x00, 0x00, enable ? (byte)0x01 : (byte)0x00 };
byte[] fill = Enumerable.Repeat(new[] { new byte(), new byte(), new byte() }, 18)
.SelectMany(colorBytes => colorBytes)
.ToArray();

byte[] leftCommand = prefix.Concat(positionL).Concat(LEDOptionOn).Concat(fill).Concat(new byte[] { 0x00, 0x3F, 0xFD }).ToArray();
byte[] rightCommand = prefix.Concat(positionR).Concat(LEDOptionOn).Concat(fill).Concat(new byte[] { 0x00, 0x3F, 0xFD }).ToArray();

WriteToSerialPort(leftCommand);
WriteToSerialPort(rightCommand);

return true;
}

public override bool SetLedBrightness(int brightness)
{
// X1 brightness range is: 1, 3, 4, convert from 0 - 100 % range
brightness = brightness == 0 ? 0 : brightness < 33 ? 1 : brightness > 66 ? 4 : 3;

// Define the HID message for setting brightness.
byte[] msg = {
0xFD, 0x3F, 0x00, 0xFD, 0x03,
0x00, 0x01, 0x05, (byte)brightness, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFD
};

// Write the SerialPort message to set the LED brightness.
WriteToSerialPort(msg);

// Turn On/Off Back LED
SetLedStatus(brightness > 0);

return true;
}

public override bool SetLedColor(Color mainColor, Color secondaryColor, LEDLevel level, int speed = 100)
{
if (!DynamicLightingCapabilities.HasFlag(level))
return false;

// Data message consists of a prefix, LED option, RGB data, and closing byte (0x00)
byte[] prefix = { 0xFD, 0x3F };
byte[] positionController = { 0x00 };
byte[] positionBackL = { 0x03 };
byte[] positionBackR = { 0x04 };
byte[] LEDOptionContoller = { 0xFE, 0x00, 0x00 };
byte[] LEDOptionBack = { 0xFE, 0x00, 0x00 };
byte[] rgbDataController = { 0x00 };
byte[] rgbDataBack = { 0x00 };

// X1 RGB seems better than OneXFly
Color ledColorController = mainColor;
Color ledColorBack = secondaryColor;

// Process Back LED here
rgbDataBack = Enumerable.Repeat(new[] { ledColorBack.R, ledColorBack.G, ledColorBack.B }, 18)
.SelectMany(colorBytes => colorBytes)
.ToArray();

// Perform functions and command build-up based on LED level
switch (level)
{
case LEDLevel.SolidColor:
// RGB data repeats 18 times, fill accordingly
rgbDataController = Enumerable
.Repeat(new[] { ledColorController.R, ledColorController.G, ledColorController.B }, 18)
.SelectMany(colorBytes => colorBytes)
.ToArray();

break;

case LEDLevel.Rainbow:
// OneXConsole "Flowing Light" effect as a rainbow effect
LEDOptionContoller = new byte[] { 0x03, 0x00, 0x00 };

// RGB data empty, repeats 54 times, fill accordingly
rgbDataController = Enumerable.Repeat((byte)0x00, 54).ToArray();

ledColorController.R = 0x00;
ledColorController.G = 0x00;

break;
}

// Combine prefix, LED Option, RGB data, and closing byte (0x00)
byte[] msgController = prefix.Concat(positionController).Concat(LEDOptionContoller).Concat(rgbDataController).Concat(new byte[] { ledColorController.R, ledColorController.G, 0x3F, 0xFD }).ToArray();
byte[] msgL = prefix.Concat(positionBackL).Concat(LEDOptionBack).Concat(rgbDataBack).Concat(new byte[] { ledColorBack.R, ledColorBack.G, 0x3F, 0xFD }).ToArray();
byte[] msgR = prefix.Concat(positionBackR).Concat(LEDOptionBack).Concat(rgbDataBack).Concat(new byte[] { ledColorBack.R, ledColorBack.G, 0x3F, 0xFD }).ToArray();

WriteToSerialPort(msgController);
WriteToSerialPort(msgL);
WriteToSerialPort(msgR);

return true;
}

public void WriteToSerialPort(byte[] data)
{
if (_serialPort is not null && _serialPort.IsOpen)
{
_queue.Enqueue(() =>
{
_serialPort.Write(data, 0, data.Length);
Task.Delay(TaskDelay).Wait();
});
}
}
}
167 changes: 167 additions & 0 deletions HandheldCompanion/Misc/SerialQueueTasks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace HandheldCompanion.Misc.Threading.Tasks
{
/**
* Source: https://github.com/Gentlee/SerialQueue
*
* C# implementation of a lightweight FIFO (First-In-First-Out) serial queue
* It is designed to provide synchronization for concurrent access to a shared resource by ensuring
* that actions or functions are executed in the order they are enqueued.
*
* When an action or function is enqueued, the `SerialQueue` acquires the spin lock to ensure exclusive access.
* It checks the reference to the last executed task to determine if there is an existing task.
*
* - If there is, it sets up a continuation using `ContinueWith` to execute the enqueued action or function after the last task completes.
* The `TaskContinuationOptions.ExecuteSynchronously` option ensures that the continuation runs on the same thread.
* - If there is no last task, a new task is created using `Task.Run` to execute the action or function.
*
* After setting up the task, the `SerialQueue` updates the reference to the last task using the weak reference.
* Finally, it releases the spin lock before returning the task.
*
* This implementation allows for efficient synchronization without blocking the caller's thread.
* It leverages the power of the Task Parallel Library (TPL) to execute actions and functions asynchronously.
*/
public class SerialQueue
{
// To Manage thread synchronization
SpinLock _spinLock = new(false);

// Maintains a weak reference to the last executed Task
readonly WeakReference<Task?> _lastTask = new(null);

/**
* Enqueues an action to be executed.
* Returns a `Task` representing the asynchronous operation.
*/
public Task Enqueue(Action action)
{
bool gotLock = false;
try
{
Task? lastTask;
Task resultTask;

_spinLock.Enter(ref gotLock);

if (_lastTask.TryGetTarget(out lastTask))
{
resultTask = lastTask.ContinueWith(_ => action(), TaskContinuationOptions.ExecuteSynchronously);
}
else
{
resultTask = Task.Run(action);
}

_lastTask.SetTarget(resultTask);

return resultTask;
}
finally
{
if (gotLock) _spinLock.Exit(false);
}
}

/**
* Enqueues a function to be executed.
* Returns a `Task<T>` representing the asynchronous operation.
*/
public Task<T> Enqueue<T>(Func<T> function)
{
bool gotLock = false;
try
{
Task? lastTask;
Task<T> resultTask;

_spinLock.Enter(ref gotLock);

if (_lastTask.TryGetTarget(out lastTask))
{
resultTask = lastTask.ContinueWith(_ => function(), TaskContinuationOptions.ExecuteSynchronously);
}
else
{
resultTask = Task.Run(function);
}

_lastTask.SetTarget(resultTask);

return resultTask;
}
finally
{
if (gotLock) _spinLock.Exit(false);
}
}

/**
* Enqueues an asynchronous action (a function that returns a `Task`) to be executed.
* Returns a `Task` representing the asynchronous operation.
*/
public Task Enqueue(Func<Task> asyncAction)
{
bool gotLock = false;
try
{
Task? lastTask;
Task resultTask;

_spinLock.Enter(ref gotLock);

if (_lastTask.TryGetTarget(out lastTask))
{
resultTask = lastTask.ContinueWith(_ => asyncAction(), TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}
else
{
resultTask = Task.Run(asyncAction);
}

_lastTask.SetTarget(resultTask);

return resultTask;
}
finally
{
if (gotLock) _spinLock.Exit(false);
}
}

/**
* Enqueues an asynchronous function (a function that returns a `Task<T>`) to be executed.
* Returns a `Task<T>` representing the asynchronous operation.
*/
public Task<T> Enqueue<T>(Func<Task<T>> asyncFunction)
{
bool gotLock = false;
try
{
Task? lastTask;
Task<T> resultTask;

_spinLock.Enter(ref gotLock);

if (_lastTask.TryGetTarget(out lastTask))
{
resultTask = lastTask.ContinueWith(_ => asyncFunction(), TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}
else
{
resultTask = Task.Run(asyncFunction);
}

_lastTask.SetTarget(resultTask);

return resultTask;
}
finally
{
if (gotLock) _spinLock.Exit(false);
}
}
}
}
Loading

0 comments on commit 9f7ab8c

Please sign in to comment.