using System.Net; using BuildSoft.OscCore; using Newtonsoft.Json; using VRC.OSCQuery; using Extensions = VRC.OSCQuery.Extensions; namespace VRC_Console; public partial class OSCCollar { private static readonly string ConfigFilePath = Path.Combine(Directory.GetCurrentDirectory(), "config.json"); public static void Main(string[] args) { OSCCollar _; if (File.Exists(ConfigFilePath)) { Config? config = JsonConvert.DeserializeObject(File.ReadAllText(ConfigFilePath)); if(config is { }) { _ = new OSCCollar(config); } else { _ = new OSCCollar(); } }else { _ = new OSCCollar(); } } private Config _config; private readonly int _portReceive; private OSCQueryService? OscQueryService { get; set; } private OscServer _server = null!; private OscClient _client = null!; private Task _consoleOutputTask; private float _leashStretch; private readonly Vector _unitVectorLeash = new(1, 0); private Vector _movementVector = new(0, 0); private bool _allowMoving = true; private DateTime _lastNilMessageSent = DateTime.UnixEpoch; private DateTime _lastOSCRefresh = DateTime.Now; private static readonly TimeSpan UpdateInterval = TimeSpan.FromMilliseconds(10); private static readonly TimeSpan MessageMinInterval = TimeSpan.FromMilliseconds(400); private GPS _gps1 = null!, _gps2 = null!, _gps3 = null!; private readonly Queue> _calibrationAverage = new(); private double _constantA, _constantB, _constantC, _constantD, _constantE; private OSCCollar(Config config, bool skipSetup = true) { this._config = config; File.WriteAllText(ConfigFilePath, JsonConvert.SerializeObject(this._config, Formatting.Indented)); this.SetupGPSVars(); if (!skipSetup) { 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_2_position, _gps2.X, _gps2.Y); Console.WriteLine(Resources.OSCCollar_OSCCollar_GPS_3_position, _gps3.X, _gps3.Y); Console.WriteLine(Resources.OSCCollar_OSCCollar_Radius_of_each_receiver, this._config.Radius * 2); Console.ReadKey(); } this._portReceive = Extensions.GetAvailableUdpPort(); this.SetupOSCServer(); this.SetupOSCQuery(); this._consoleOutputTask = this.SetupConsoleOutput(); } private OSCCollar(string ip = "127.0.0.1", int portSend = 9000, double radius = 100, double calibrationX = 0, double calibrationY = 0, double walkStretchDeadzone = 0.1, double runStretch = 0.4, bool skipSetup = true) : this(new Config() { Ip = ip, PortSend = portSend, WalkStretchDeadzone = walkStretchDeadzone, RunStretch = runStretch, Radius = radius, CalibrationX = calibrationX, CalibrationY = calibrationY }, skipSetup) { } #region Setup private void SetupGPSVars() { this._gps1 = new(0, -this._config.Radius); this._gps2 = new(-(this._config.Radius * 0.866), this._config.Radius / 2); this._gps3 = new(this._config.Radius * 0.866, this._config.Radius / 2); this._constantA = 2 * _gps2.X - 2 * _gps1.X; this._constantB = 2 * _gps2.Y - 2 * _gps1.Y; this._constantC = Math.Pow(_gps1.X, 2) + Math.Pow(_gps2.X, 2) - Math.Pow(_gps1.Y, 2) + Math.Pow(_gps2.Y, 2); this._constantD = 2 * _gps3.X - 2 * _gps2.X; this._constantE = Math.Pow(_gps2.X, 2) + Math.Pow(_gps3.X, 2) - Math.Pow(_gps2.Y, 2) + Math.Pow(_gps3.Y, 2); } private void SetupOSCServer() { this._server = new OscServer(this._portReceive); this._client = new OscClient(this._config.Ip, this._config.PortSend); this._server.TryAddMethod("/avatar/parameters/GPS1", GPS1Handle); this._server.TryAddMethod("/avatar/parameters/GPS2", GPS2Handle); this._server.TryAddMethod("/avatar/parameters/GPS3", GPS3Handle); this._server.TryAddMethod("/avatar/parameters/Leash_Stretch", CollarStretchHandle); this._server.TryAddMethod("/avatar/parameters/Leash_Toggle", AllowMovingHandle); this._server.Start(); Thread runningThread = new Thread(RunningThread); runningThread.Start(); } private void SetupOSCQuery() { var tcpPort = Extensions.GetAvailableTcpPort(); this.OscQueryService = new OSCQueryServiceBuilder() .WithHostIP(IPAddress.Parse(this._config.Ip)) .WithOscIP(IPAddress.Parse(this._config.Ip)) .WithDiscovery(new MeaModDiscovery()) .WithTcpPort(tcpPort) .WithUdpPort(this._portReceive) .WithServiceName("Collar") .StartHttpServer() .AdvertiseOSC() .AdvertiseOSCQuery() .Build(); this.OscQueryService.AddEndpoint("/avatar/parameters/GPS1", Attributes.AccessValues.WriteOnly); this.OscQueryService.AddEndpoint("/avatar/parameters/GPS2", Attributes.AccessValues.WriteOnly); this.OscQueryService.AddEndpoint("/avatar/parameters/GPS3", Attributes.AccessValues.WriteOnly); this.OscQueryService.AddEndpoint("/avatar/parameters/Leash_Stretch", Attributes.AccessValues.WriteOnly); this.OscQueryService.AddEndpoint("/avatar/parameters/Leash_Toggle", Attributes.AccessValues.WriteOnly); this.OscQueryService.RefreshServices(); } #endregion #region Handle OSC-Messages private void AllowMovingHandle(OscMessageValues messageValues) { this._allowMoving = messageValues.ReadBooleanElement(0); } private void GPS1Handle(OscMessageValues messageValues) { this._gps1.Distance = messageValues.ReadFloatElement(0) * this._config.Radius; } private void GPS2Handle(OscMessageValues messageValues) { this._gps2.Distance = messageValues.ReadFloatElement(0) * this._config.Radius; } private void GPS3Handle(OscMessageValues messageValues) { this._gps3.Distance = messageValues.ReadFloatElement(0) * this._config.Radius; } private void CollarStretchHandle(OscMessageValues messageValues) { this._leashStretch = messageValues.ReadFloatElement(0); } #endregion private void CalculatePositionFromGPS() { double gpsFactorA = Math.Pow(_gps1.Distance, 2) - Math.Pow(_gps2.Distance, 2) - _constantC; double gpsFactorB = Math.Pow(_gps2.Distance, 2) - Math.Pow(_gps3.Distance, 2) - _constantE; double gpsPosX = (gpsFactorB / _constantD); double gpsPosY = (gpsFactorA * _constantD - _constantA * gpsFactorB) / (_constantB * _constantD); AddCalibrationValues(gpsPosX, gpsPosY); double posX = gpsPosX + this._config.CalibrationX; double posY = gpsPosY + this._config.CalibrationY; double inverseLength = 1 / Math.Sqrt(Math.Pow(posX, 2) + Math.Pow(posY, 2)); _unitVectorLeash.X = posX * inverseLength * -1; _unitVectorLeash.Y = posY * inverseLength; if (_leashStretch < this._config.WalkStretchDeadzone) //Below Deadzone _movementVector = new(); else if (_leashStretch < this._config.RunStretch) //Walk _movementVector = _unitVectorLeash.MultipliedWith(0.5); else //Run _movementVector = _unitVectorLeash.MultipliedWith(1); } private void RunningThread() { while (true) { this._server.Update(); CalculatePositionFromGPS(); if (_lastNilMessageSent.Add(MessageMinInterval) < DateTime.Now) { this._client.Send("/input/Vertical", 0f); this._client.Send("/input/Horizontal", 0f); this._lastNilMessageSent = DateTime.Now; }else if (_allowMoving) { this._client.Send("/input/Vertical", Convert.ToSingle(_movementVector.Y)); this._client.Send("/input/Horizontal", Convert.ToSingle(_movementVector.X)); } if (_lastOSCRefresh.Add(TimeSpan.FromSeconds(10)) > DateTime.Now) { this.OscQueryService?.RefreshServices(); _lastOSCRefresh = DateTime.Now; } Thread.Sleep(UpdateInterval); } } private void AddCalibrationValues(double x, double y) { this._calibrationAverage.Enqueue(new ValueTuple(x, y)); if (this._calibrationAverage.Count > 50) this._calibrationAverage.Dequeue(); } private ValueTuple GetCalibrationValues() { double averageX = -this._calibrationAverage.Average(value => value.Item1); double averageY = -this._calibrationAverage.Average(value => value.Item2); return new ValueTuple(averageX, averageY); } }