Add option to set endpoints for stretch and toggle

This commit is contained in:
Glax 2024-09-06 00:51:28 +02:00
parent 9ab3d75e7b
commit 5f8b5f8112
6 changed files with 105 additions and 95 deletions

View File

@ -1,11 +1,21 @@
namespace VRC_Console; using Newtonsoft.Json;
namespace VRC_Console;
internal class Config internal class Config
{ {
internal static readonly string ConfigFilePath = Path.Combine(Directory.GetCurrentDirectory(), "config.json");
public string Ip = "127.0.0.1"; public string Ip = "127.0.0.1";
public int PortSend = 9000; public int PortSend = 9000;
public double Radius = 100; public double Radius = 100;
public double WalkStretchDeadzone = 0.1; public double WalkStretchDeadzone = 0.1;
public double RunStretch = 0.4; public double RunStretch = 0.4;
public int CalibrationValues = 128; public int CalibrationValues = 128;
public string LeashStretchParameter = "Leash/Leash_Stretch";
public string LeashToggleParameter = "Leash/Toggle";
public Config()
{
File.WriteAllText(ConfigFilePath, JsonConvert.SerializeObject(this, Formatting.Indented));
}
} }

View File

@ -1,28 +1,44 @@
using Spectre.Console; using System.Runtime.InteropServices;
namespace VRC_Console; namespace VRC_Console;
public partial class OSCCollar public partial class OSCCollar
{ {
private const byte CoordinateSystemSize = 16; private const byte CoordinateSystemSize = 16;
private const int MinWindowHeight = 10;
private const int MinWindowWidth = 60;
private void CheckConsoleMinSize()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return;
if (Console.WindowHeight < MinWindowHeight)
Console.WindowHeight = MinWindowHeight;
if (Console.WindowWidth < MinWindowWidth)
Console.WindowWidth = MinWindowWidth;
}
private void PrintConsole() private void PrintConsole()
{ {
Console.Clear(); Console.Clear();
CheckConsoleMinSize();
ValueTuple<string, string>[] configValues = new[] ValueTuple<string, string>[] configValues = new[]
{ {
new ValueTuple<string, string>("IP", this._config.Ip), new ValueTuple<string, string>("IP", Config.Ip),
new ValueTuple<string, string>("Send-Osc-Port", this._config.PortSend.ToString()), new ValueTuple<string, string>("Send-Osc-Port", Config.PortSend.ToString()),
new ValueTuple<string, string>("Receive-Osc-Port", this._osc.OSCPort.ToString()), new ValueTuple<string, string>("Receive-Osc-Port", this._osc.OSCPort.ToString()),
new ValueTuple<string, string>("Calibration Values", $"{this._calibrationAverage.Count}/{this._config.CalibrationValues}"), new ValueTuple<string, string>("Calibration Values", $"{this._calibrationAverage.Count}/{Config.CalibrationValues}"),
new ValueTuple<string, string>("Stretch Parameter", Config.LeashStretchParameter),
new ValueTuple<string, string>("Toggle Parameter", Config.LeashToggleParameter),
new ValueTuple<string, string>("AvatarId", _avatarId.Length < 1 ? "Reset Avatar to show." : _avatarId),
}; };
ValueTuple<string, string>[] variableValues = new[] ValueTuple<string, string>[] variableValues = new[]
{ {
new ValueTuple<string, string>("Status", $"{(_allowMoving ? Resources.OSCCollar_ConsoleOutput_StatusEnabled : Resources.OSCCollar_ConsoleOutput_StatusDisabled)}"), new ValueTuple<string, string>("Status", $"{(_allowMoving ? Resources.OSCCollar_ConsoleOutput_StatusEnabled : Resources.OSCCollar_ConsoleOutput_StatusDisabled)}"),
new ValueTuple<string, string>("GPS 1", $"{_gps1.Distance: 0.00000}"), new ValueTuple<string, string>("GPS 1", $"{refPos1.Distance: 0.00000}"),
new ValueTuple<string, string>("GPS 2", $"{_gps2.Distance: 0.00000}"), new ValueTuple<string, string>("GPS 2", $"{refPos2.Distance: 0.00000}"),
new ValueTuple<string, string>("GPS 3", $"{_gps3.Distance: 0.00000}"), new ValueTuple<string, string>("GPS 3", $"{refPos3.Distance: 0.00000}"),
new ValueTuple<string, string>(Resources.OSCCollar_ConsoleOutput_PositionVector, _unitVectorLeash.ToString()), new ValueTuple<string, string>(Resources.OSCCollar_ConsoleOutput_PositionVector, _unitVectorLeash.ToString()),
new ValueTuple<string, string>(Resources.OSCCollar_ConsoleOutput_LeashStretch, _leashStretch.ToString(" 0.00000")), new ValueTuple<string, string>(Resources.OSCCollar_ConsoleOutput_LeashStretch, _leashStretch.ToString(" 0.00000")),
new ValueTuple<string, string>(Resources.OSCCollar_ConsoleOutput_MovementVector, _movementVector.ToString()), new ValueTuple<string, string>(Resources.OSCCollar_ConsoleOutput_MovementVector, _movementVector.ToString()),
@ -57,7 +73,7 @@ public partial class OSCCollar
} }
Console.SetCursorPosition(0, Console.WindowHeight - 2); Console.SetCursorPosition(0, Console.WindowHeight - 2);
Console.Write($"Last Hand Reset: {DateTime.Now.Subtract(_lastNilMessageSent):ss\\.fff}\t\tAvatarId: {(_avatarId.Length < 1 ? "Reset Avatar to show." : _avatarId)}\t\tRuntime: {DateTime.Now.Subtract(_programStarted):hh\\:mm\\:ss\\.fff}"); Console.Write($"Last Hand Reset: {DateTime.Now.Subtract(_lastNilMessageSent):ss\\.fff}\tLast Message Received: {(_lastMessageReceived == DateTime.UnixEpoch ? "never" : DateTime.Now.Subtract(_lastMessageReceived)):ss\\.fff} {_lastMessageReceivedParameter}\tRuntime: {DateTime.Now.Subtract(_programStarted):hh\\:mm\\:ss\\.fff}");
} }
private String GetCoordinateSystem() private String GetCoordinateSystem()

View File

@ -5,29 +5,13 @@ namespace VRC_Console;
public partial class OSCCollar public partial class OSCCollar
{ {
private static readonly string ConfigFilePath = Path.Combine(Directory.GetCurrentDirectory(), "config.json");
public static void Main(string[] args) public static void Main(string[] args)
{
OSCCollar _;
if (File.Exists(ConfigFilePath))
{
Config? config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(ConfigFilePath));
if(config is { })
{
_ = new OSCCollar(config);
}
else
{ {
_ = new OSCCollar(); _ = new OSCCollar();
} }
}else
{
_ = new OSCCollar();
}
}
private readonly Config _config; private static readonly Config Config = File.Exists(Config.ConfigFilePath) ? JsonConvert.DeserializeObject<Config>(File.ReadAllText(Config.ConfigFilePath))! : new Config();
private readonly OSC _osc; private readonly OSC _osc;
private Task _consoleOutputTask; private Task _consoleOutputTask;
@ -38,25 +22,24 @@ public partial class OSCCollar
private Vector _movementVector = new(0, 0); private Vector _movementVector = new(0, 0);
private bool _allowMoving = true; private bool _allowMoving = true;
private DateTime _lastNilMessageSent = DateTime.UnixEpoch; private DateTime _lastNilMessageSent = DateTime.UnixEpoch;
private DateTime _lastMessageReceived = DateTime.UnixEpoch;
private string _lastMessageReceivedParameter = "null";
private readonly DateTime _programStarted = DateTime.Now; private readonly DateTime _programStarted = DateTime.Now;
private static readonly TimeSpan UpdateInterval = TimeSpan.FromMilliseconds(10); private static readonly TimeSpan UpdateInterval = TimeSpan.FromMilliseconds(10); //Minimum is 10ms for VRC to register
private static readonly TimeSpan MessageMinInterval = TimeSpan.FromMilliseconds(400); private static readonly TimeSpan MessageMinInterval = TimeSpan.FromMilliseconds(400); //Reset OSC movement at least once per second, to prevent hands from resetting to desktop position
private GPS _gps1 = null!, _gps2 = null!, _gps3 = null!; private ReferencePosition refPos1 = null!, refPos2 = null!, refPos3 = null!;
private readonly Queue<ValueTuple<double, double>> _calibrationAverage = new(); private readonly Queue<ValueTuple<double, double>> _calibrationAverage = new();
private double _constantA, _constantB, _constantC, _constantD, _constantE; private double _constantA, _constantB, _constantC, _constantD, _constantE;
private OSCCollar(Config config, bool skipSetup = true) private OSCCollar(bool skipSetup = true)
{ {
this._config = config; this._osc = new OSC("OSCCollar", new List<OSCEndpoint>()
File.WriteAllText(ConfigFilePath, JsonConvert.SerializeObject(this._config, Formatting.Indented));
this._osc = new OSC("OSCCollar", new Dictionary<string, Type>()
{ {
{ "/avatar/parameters/GPS1", typeof(float) }, new("/avatar/parameters/GPS1", typeof(float), OnGPSParameterChanged),
{ "/avatar/parameters/GPS2", typeof(float) }, new("/avatar/parameters/GPS2", typeof(float), OnGPSParameterChanged),
{ "/avatar/parameters/GPS3", typeof(float) }, new("/avatar/parameters/GPS3", typeof(float), OnGPSParameterChanged),
{ "/avatar/parameters/Leash_Stretch", typeof(float) }, new($"/avatar/parameters/{Config.LeashStretchParameter}", typeof(float), OnLeashStretchChanged),
{ "/avatar/parameters/Leash_Toggle", typeof(bool) }, new($"/avatar/parameters/{Config.LeashToggleParameter}", typeof(bool), OnLeashToggled),
}); });
this._osc.OnParameterChangeEvent += OnOscParameterChangeEvent; this._osc.OnParameterChangeEvent += OnOscParameterChangeEvent;
@ -64,10 +47,10 @@ public partial class OSCCollar
if (!skipSetup) if (!skipSetup)
{ {
Console.WriteLine(Resources.OSCCollar_OSCCollar_Position_your_GPS_receivers); Console.WriteLine(Resources.OSCCollar_OSCCollar_Position_your_GPS_receivers);
Console.WriteLine(Resources.OSCCollar_OSCCollar_GPS_1_position, _gps1.X, _gps1.Y); Console.WriteLine(Resources.OSCCollar_OSCCollar_GPS_1_position, refPos1.X, refPos1.Y);
Console.WriteLine(Resources.OSCCollar_OSCCollar_GPS_2_position, _gps2.X, _gps2.Y); Console.WriteLine(Resources.OSCCollar_OSCCollar_GPS_2_position, refPos2.X, refPos2.Y);
Console.WriteLine(Resources.OSCCollar_OSCCollar_GPS_3_position, _gps3.X, _gps3.Y); Console.WriteLine(Resources.OSCCollar_OSCCollar_GPS_3_position, refPos3.X, refPos3.Y);
Console.WriteLine(Resources.OSCCollar_OSCCollar_Radius_of_each_receiver, this._config.Radius * 2); Console.WriteLine(Resources.OSCCollar_OSCCollar_Radius_of_each_receiver, Config.Radius * 2);
Console.ReadKey(); Console.ReadKey();
} }
@ -75,85 +58,86 @@ public partial class OSCCollar
runningThread.Start(); runningThread.Start();
} }
private void OnOscParameterChangeEvent(string endpoint, object? oldvalue, object? newvalue) private void OnLeashToggled(string endpoint, object? oldValue, object? newValue)
{
this._allowMoving = (bool)(newValue ?? false);
}
private void OnLeashStretchChanged(string endpoint, object? oldValue, object? newValue)
{
this._leashStretch = (float)(newValue ?? 0);
}
private void OnGPSParameterChanged(string endpoint, object? oldValue, object? newValue)
{ {
switch (endpoint) switch (endpoint)
{ {
case "/avatar/parameters/GPS1": case "/avatar/parameters/GPS1":
this._gps1.Distance = 1 - (float)(newvalue ?? 1); this.refPos1.Distance = 1 - (float)(newValue ?? 1);
break; break;
case "/avatar/parameters/GPS2": case "/avatar/parameters/GPS2":
this._gps2.Distance = 1 - (float)(newvalue ?? 1); this.refPos2.Distance = 1 - (float)(newValue ?? 1);
break; break;
case "/avatar/parameters/GPS3": case "/avatar/parameters/GPS3":
this._gps3.Distance = 1 - (float)(newvalue ?? 1); this.refPos3.Distance = 1 - (float)(newValue ?? 1);
break;
case "/avatar/parameters/Leash_Stretch":
this._leashStretch = (float)(newvalue ?? 0);
break;
case "/avatar/parameters/Leash_Toggle":
this._allowMoving = (bool)(newvalue ?? false);
break; break;
}
}
private void OnOscParameterChangeEvent(string endpoint, object? oldValue, object? newValue)
{
_lastMessageReceived = DateTime.Now;
_lastMessageReceivedParameter = endpoint;
switch (endpoint)
{
case "/avatar/change": case "/avatar/change":
this._avatarId = (string)(newvalue ?? ""); this._avatarId = (string)(newValue ?? "");
break; break;
} }
} }
private OSCCollar(string ip = "127.0.0.1", int portSend = 9000, double radius = 100, double walkStretchDeadzone = 0.1, double runStretch = 0.4, int calibrationValues = 128,
bool skipSetup = true) : this(new Config()
{
Ip = ip,
PortSend = portSend,
WalkStretchDeadzone = walkStretchDeadzone,
RunStretch = runStretch,
Radius = radius,
CalibrationValues = calibrationValues
}, skipSetup)
{
}
#region Setup #region Setup
private void SetupGPSVars() private void SetupGPSVars()
{ {
this._gps1 = new(0, -this._config.Radius); this.refPos1 = new(0, -Config.Radius);
this._gps2 = new(-(this._config.Radius * 0.866), this._config.Radius / 2); this.refPos2 = new(-(Config.Radius * 0.866), Config.Radius / 2);
this._gps3 = new(this._config.Radius * 0.866, this._config.Radius / 2); this.refPos3 = new(Config.Radius * 0.866, Config.Radius / 2);
this._constantA = 2 * _gps2.X - 2 * _gps1.X; this._constantA = 2 * refPos2.X - 2 * refPos1.X;
this._constantB = 2 * _gps2.Y - 2 * _gps1.Y; this._constantB = 2 * refPos2.Y - 2 * refPos1.Y;
this._constantC = Math.Pow(_gps1.X, 2) + Math.Pow(_gps2.X, 2) - Math.Pow(_gps1.Y, 2) + Math.Pow(_gps2.Y, 2); this._constantC = Math.Pow(refPos1.X, 2) + Math.Pow(refPos2.X, 2) - Math.Pow(refPos1.Y, 2) + Math.Pow(refPos2.Y, 2);
this._constantD = 2 * _gps3.X - 2 * _gps2.X; this._constantD = 2 * refPos3.X - 2 * refPos2.X;
this._constantE = Math.Pow(_gps2.X, 2) + Math.Pow(_gps3.X, 2) - Math.Pow(_gps2.Y, 2) + Math.Pow(_gps3.Y, 2); this._constantE = Math.Pow(refPos2.X, 2) + Math.Pow(refPos3.X, 2) - Math.Pow(refPos2.Y, 2) + Math.Pow(refPos3.Y, 2);
} }
#endregion #endregion
private void CalculatePositionFromGPS() private void CalculatePositionFromGPS()
{ {
if (this._gps1.Distance == 0 || this._gps2.Distance == 0 || this._gps3.Distance == 0) if (this.refPos1.Distance == 0 || this.refPos2.Distance == 0 || this.refPos3.Distance == 0)
return; return;
double gpsFactorA = Math.Pow(_gps1.Distance, 2) - Math.Pow(_gps2.Distance, 2) - _constantC; double factorA = Math.Pow(refPos1.Distance, 2) - Math.Pow(refPos2.Distance, 2) - _constantC;
double gpsFactorB = Math.Pow(_gps2.Distance, 2) - Math.Pow(_gps3.Distance, 2) - _constantE; double factorB = Math.Pow(refPos2.Distance, 2) - Math.Pow(refPos3.Distance, 2) - _constantE;
double gpsPosX = (gpsFactorB / _constantD); double calculatedPosX = (factorB / _constantD);
double gpsPosY = -1 * (gpsFactorA * _constantD - _constantA * gpsFactorB) / (_constantB * _constantD); double calculatedPosY = -1 * (factorA * _constantD - _constantA * factorB) / (_constantB * _constantD);
if (this._leashStretch == 0) if (this._leashStretch == 0)
AddCalibrationValues(gpsPosX, gpsPosY); AddCalibrationValues(calculatedPosX, calculatedPosY);
ValueTuple<double, double> calibrationValues = GetCalibrationValues(); ValueTuple<double, double> calibrationValues = GetCalibrationValues();
double posX = gpsPosX + calibrationValues.Item1; double calibratedPosX = calculatedPosX + calibrationValues.Item1;
double posY = gpsPosY + calibrationValues.Item2; double calibratedPosY = calculatedPosY + calibrationValues.Item2;
double inverseLength = 1 / Math.Sqrt(Math.Pow(posX, 2) + Math.Pow(posY, 2)); double inverseLength = 1 / Math.Sqrt(Math.Pow(calibratedPosX, 2) + Math.Pow(calibratedPosY, 2));
_unitVectorLeash.X = inverseLength is not Double.PositiveInfinity ? posX * inverseLength : 0; _unitVectorLeash.X = inverseLength is not Double.PositiveInfinity ? calibratedPosX * inverseLength : 0;
_unitVectorLeash.Y = inverseLength is not Double.PositiveInfinity ? posY * inverseLength : 0; _unitVectorLeash.Y = inverseLength is not Double.PositiveInfinity ? calibratedPosY * inverseLength : 0;
if (_leashStretch < this._config.WalkStretchDeadzone) //Below Deadzone if (_leashStretch < Config.WalkStretchDeadzone) //Below Deadzone
_movementVector = new(); _movementVector = new();
else if (_leashStretch < this._config.RunStretch) //Walk else if (_leashStretch < Config.RunStretch) //Walk
_movementVector = _unitVectorLeash.MultipliedWith(0.5); _movementVector = _unitVectorLeash.MultipliedWith(0.5);
else //Run else //Run
_movementVector = _unitVectorLeash.MultipliedWith(1); _movementVector = _unitVectorLeash.MultipliedWith(1);
@ -183,7 +167,7 @@ public partial class OSCCollar
private void AddCalibrationValues(double x, double y) private void AddCalibrationValues(double x, double y)
{ {
this._calibrationAverage.Enqueue(new ValueTuple<double, double>(x, y)); this._calibrationAverage.Enqueue(new ValueTuple<double, double>(x, y));
while (this._calibrationAverage.Count > this._config.CalibrationValues) while (this._calibrationAverage.Count > Config.CalibrationValues)
this._calibrationAverage.Dequeue(); this._calibrationAverage.Dequeue();
} }

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>VRC_Console</RootNamespace> <RootNamespace>VRC_Console</RootNamespace>

View File

@ -1,12 +1,12 @@
namespace VRC_Console; namespace VRC_Console;
public class GPS public class ReferencePosition
{ {
public double X { get; init; } public double X { get; init; }
public double Y { get; init; } public double Y { get; init; }
public double Distance { get; set; } public double Distance { get; set; }
public GPS(double x, double y) public ReferencePosition(double x, double y)
{ {
this.X = x; this.X = x;
this.Y = y; this.Y = y;

View File

@ -19,7 +19,7 @@ public class Vector
public override string ToString() public override string ToString()
{ {
return $"({this.X:' '0.00000;'-'0.00000} {this.Y:' '0.00000;'-'0.00000})"; return $"(Vector X={this.X:' '0.00000;'-'0.00000} Y={this.Y:' '0.00000;'-'0.00000})";
} }
public Vector MultipliedWith(double factor) public Vector MultipliedWith(double factor)