diff --git a/SDK.CSharp.Example/Program.cs b/SDK.CSharp.Example/Program.cs index 256fa6c..1b6c526 100644 --- a/SDK.CSharp.Example/Program.cs +++ b/SDK.CSharp.Example/Program.cs @@ -4,28 +4,79 @@ using OpenShock.SDK.CSharp; using OpenShock.SDK.CSharp.Hub; using OpenShock.SDK.CSharp.Live; +using OpenShock.SDK.CSharp.Models; +using Serilog; + +const string apiToken = ""; +var deviceId = Guid.Parse("bc849182-89e0-43ff-817b-32400be3f97d"); var hostBuilder = Host.CreateDefaultBuilder(); +var loggerConfiguration = new LoggerConfiguration() + .MinimumLevel.Verbose() + .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Information) + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"); + +Log.Logger = loggerConfiguration.CreateLogger(); + +hostBuilder.UseSerilog(Log.Logger); + var host = hostBuilder.Build(); -// var apiClient = new OpenShockApiClient(new ApiClientOptions() -// { -// Token = "vYqcHzz0XeALfo3vzQD4Wh7KjqbJeuvZsPz8jlJrtBlfGTF9qKhxtKSrzvZO1A53" -// }); -// -// var a = await apiClient.GetOwnShockers(); -// -// var b = await apiClient.GetDeviceGateway(Guid.Parse("bc849182-89e0-43ff-817b-32400be3f97d")); +var logger = host.Services.GetRequiredService>(); + +var apiClient = new OpenShockApiClient(new ApiClientOptions +{ + Token = apiToken +}); + +var shockers = await apiClient.GetOwnShockers(); + +if (!shockers.IsT0) +{ + logger.LogError("Failed to get own shockers, make sure you used a valid api token"); + return; +} -var apiLiveClient = new OpenShockHubClient(new HubClientOptions() +var apiSignalRHubClient = new OpenShockHubClient(new HubClientOptions { - Token = "71WZxCwCAIBJNgNG2pgdaHxHdaipUKmA6MalZUXNZhv3IkV7GB1ObxA35ud4tkPz" + Token = apiToken, + ConfigureLogging = builder => builder.AddSerilog(Log.Logger) }); -await apiLiveClient.StartAsync(); +await apiSignalRHubClient.StartAsync(); -OpenShockLiveControlClient controlClient = new("de1-gateway.shocklink.net", Guid.Parse("bc849182-89e0-43ff-817b-32400be3f97d"), "71WZxCwCAIBJNgNG2pgdaHxHdaipUKmA6MalZUXNZhv3IkV7GB1ObxA35ud4tkPz", host.Services.GetRequiredService>()); +var gatewayRequest = await apiClient.GetDeviceGateway(deviceId); + +if (gatewayRequest.IsT1) +{ + logger.LogError("Failed to get gateway, make sure you used a valid device id"); + return; +} + +if (gatewayRequest.IsT2) +{ + logger.LogError("Device is offline"); + return; +} + +if (gatewayRequest.IsT3) +{ + logger.LogError("Device is not connected to a gateway"); + return; +} + +var gateway = gatewayRequest.AsT0.Value; + +logger.LogInformation("Device is connected to gateway {GatewayId} in region {Region}", gateway.Gateway, gateway.Country); + +OpenShockLiveControlClient controlClient = new(gateway.Gateway, deviceId, apiToken, host.Services.GetRequiredService>()); await controlClient.InitializeAsync(); -await Task.Delay(-1); \ No newline at end of file +while (true) +{ + Console.ReadLine(); + controlClient.IntakeFrame(Guid.Parse("d9267ca6-d69b-4b7a-b482-c455f75a4408"), ControlType.Vibrate, 100); + Console.WriteLine("Sent frame"); +} \ No newline at end of file diff --git a/SDK.CSharp.Example/SDK.CSharp.Example.csproj b/SDK.CSharp.Example/SDK.CSharp.Example.csproj index a678c5b..941ab33 100644 --- a/SDK.CSharp.Example/SDK.CSharp.Example.csproj +++ b/SDK.CSharp.Example/SDK.CSharp.Example.csproj @@ -14,6 +14,9 @@ + + + diff --git a/SDK.CSharp.Live/IOpenShockLiveControlClient.cs b/SDK.CSharp.Live/IOpenShockLiveControlClient.cs index f15e2d4..8e25657 100644 --- a/SDK.CSharp.Live/IOpenShockLiveControlClient.cs +++ b/SDK.CSharp.Live/IOpenShockLiveControlClient.cs @@ -1,4 +1,5 @@ using OpenShock.SDK.CSharp.Live.LiveControlModels; +using OpenShock.SDK.CSharp.Models; using OpenShock.SDK.CSharp.Updatables; namespace OpenShock.SDK.CSharp.Live; @@ -21,7 +22,13 @@ public interface IOpenShockLiveControlClient #region Send Methods - public Task SendFrame(ClientLiveFrame frame); + /// + /// Intake a shocker frame, and send it to the server whenever a tick happens. + /// + /// + /// + /// + public void IntakeFrame(Guid shocker, ControlType type, byte intensity); #endregion } \ No newline at end of file diff --git a/SDK.CSharp.Live/LiveControlModels/LiveRequestType.cs b/SDK.CSharp.Live/LiveControlModels/LiveRequestType.cs index b798465..99f5aad 100644 --- a/SDK.CSharp.Live/LiveControlModels/LiveRequestType.cs +++ b/SDK.CSharp.Live/LiveControlModels/LiveRequestType.cs @@ -3,6 +3,7 @@ public enum LiveRequestType { Frame = 0, + BulkFrame = 1, Pong = 1000 } \ No newline at end of file diff --git a/SDK.CSharp.Live/LiveControlModels/TpsData.cs b/SDK.CSharp.Live/LiveControlModels/TpsData.cs new file mode 100644 index 0000000..0612e85 --- /dev/null +++ b/SDK.CSharp.Live/LiveControlModels/TpsData.cs @@ -0,0 +1,9 @@ +namespace OpenShock.SDK.CSharp.Live.LiveControlModels; + +/// +/// TPS information +/// +public sealed class TpsData +{ + public required byte Client { get; set; } +} \ No newline at end of file diff --git a/SDK.CSharp.Live/OpenShockLiveControlClient.cs b/SDK.CSharp.Live/OpenShockLiveControlClient.cs index f38040a..18034fa 100644 --- a/SDK.CSharp.Live/OpenShockLiveControlClient.cs +++ b/SDK.CSharp.Live/OpenShockLiveControlClient.cs @@ -1,4 +1,6 @@ -using System.Net.WebSockets; +using System.Collections.Concurrent; +using System.ComponentModel.DataAnnotations; +using System.Net.WebSockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,6 +11,7 @@ using OneOf.Types; using OpenShock.SDK.CSharp.Live.LiveControlModels; using OpenShock.SDK.CSharp.Live.Utils; +using OpenShock.SDK.CSharp.Models; using OpenShock.SDK.CSharp.Serialization; using OpenShock.SDK.CSharp.Updatables; using OpenShock.SDK.CSharp.Utils; @@ -25,12 +28,49 @@ public sealed class OpenShockLiveControlClient : IOpenShockLiveControlClient, IA public string Gateway { get; } public Guid DeviceId { get; } - + private readonly string _authToken; private readonly ILogger _logger; private readonly ApiClientOptions.ProgramInfo? _programInfo; private ClientWebSocket? _clientWebSocket = null; + private sealed class ShockerState + { + public ControlType LastType { get; set; } = ControlType.Stop; + [Range(0, 100)] public byte LastIntensity { get; set; } = 0; + + /// + /// Active until time for the shocker, determined by client TPS interval + current time + /// + public DateTimeOffset ActiveUntil = DateTimeOffset.MinValue; + } + + private Timer _managedFrameTimer; + + private ConcurrentDictionary _shockerStates = new(); + + private byte _tps = 0; + + public byte Tps + { + get => _tps; + private set + { + _tps = value; + + if (_tps == 0) + { + _managedFrameTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + return; + } + + var interval = TimeSpan.FromMilliseconds(1000d / _tps); + _managedFrameTimer.Change(interval, interval); + _logger.LogDebug("Managed frame timer interval set {Tps} TPS / {Interval}ms interval", _tps, + interval.Milliseconds); + } + } + public event Func? OnDeviceNotConnected; public event Func? OnDeviceConnected; public event Func? OnDispose; @@ -53,6 +93,8 @@ public OpenShockLiveControlClient(string gateway, Guid deviceId, string authToke _dispose = new CancellationTokenSource(); _linked = _dispose; + + _managedFrameTimer = new Timer(FrameTimerTick); } public Task InitializeAsync() => ConnectAsync(); @@ -155,7 +197,7 @@ private string GetUserAgent() string programName; Version programVersion; - + if (_programInfo == null) { (programName, programVersion) = UserAgentUtils.GetAssemblyInfo(); @@ -189,8 +231,9 @@ private async Task ReceiveLoop() } var message = - await JsonWebSocketUtils.ReceiveFullMessageAsyncNonAlloc>( - _clientWebSocket, _linked.Token, JsonSerializerOptions); + await JsonWebSocketUtils + .ReceiveFullMessageAsyncNonAlloc>( + _clientWebSocket, _linked.Token, JsonSerializerOptions); if (message.IsT2) { @@ -263,7 +306,7 @@ await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Normal cl } - private async Task HandleMessage(BaseResponse? wsRequest) + private async Task HandleMessage(LiveControlModels.BaseResponse? wsRequest) { if (wsRequest == null) return; switch (wsRequest.ResponseType) @@ -311,9 +354,83 @@ await QueueMessage(new BaseRequest case LiveResponseType.DeviceConnected: await OnDeviceConnected.Raise(); break; + + case LiveResponseType.TPS: + if (wsRequest.Data == null) + { + _logger.LogWarning("TPS response data is null"); + return; + } + + var tpsDataResponse = wsRequest.Data.Deserialize(JsonSerializerOptions); + if (tpsDataResponse == null) + { + _logger.LogWarning("TPS response data failed to deserialize"); + return; + } + + _logger.LogDebug("Received TPS: {Tps}", Tps); + + Tps = tpsDataResponse.Client; + break; } } + private async void FrameTimerTick(object state) + { + try + { + if (_clientWebSocket is not { State: WebSocketState.Open }) + { + _logger.LogWarning("Frame timer ticked, but websocket is not open"); + _managedFrameTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + return; + } + + await QueueMessage(new BaseRequest() + { + RequestType = LiveRequestType.BulkFrame, + Data = _shockerStates.Where(x => x.Value.ActiveUntil > DateTimeOffset.UtcNow) + .Select(x => new ClientLiveFrame + { + Shocker = x.Key, + Type = x.Value.LastType, + Intensity = x.Value.LastIntensity + }) + }); + } + catch (Exception e) + { + _logger.LogError(e, "Error in managed frame timer callback"); + } + } + + /// + public void IntakeFrame(Guid shocker, ControlType type, byte intensity) + { + if (_tps == 0) + { + _logger.LogWarning("Intake frame called, but TPS is 0"); + return; + } + + var activeUntil = DateTimeOffset.UtcNow.AddMilliseconds(1000d / Tps * 2.5); + + _shockerStates.AddOrUpdate(shocker, new ShockerState() + { + LastIntensity = intensity, + ActiveUntil = activeUntil, + LastType = type + }, (guid, shockerState) => + { + shockerState.LastIntensity = intensity; + shockerState.ActiveUntil = activeUntil; + shockerState.LastType = type; + return shockerState; + }); + } + + private bool _disposed = false; public async ValueTask DisposeAsync() @@ -360,13 +477,4 @@ public Task Run(Task? function, CancellationToken cancellationToken = default, [ private readonly AsyncUpdatableVariable _latency = new(0); public IAsyncUpdatable Latency => _latency; - - public async Task SendFrame(ClientLiveFrame frame) - { - await QueueMessage(new BaseRequest() - { - RequestType = LiveRequestType.Frame, - Data = frame - }); - } } \ No newline at end of file diff --git a/SDK.CSharp.Live/SDK.CSharp.Live.csproj b/SDK.CSharp.Live/SDK.CSharp.Live.csproj index 7b1732b..72191e3 100644 --- a/SDK.CSharp.Live/SDK.CSharp.Live.csproj +++ b/SDK.CSharp.Live/SDK.CSharp.Live.csproj @@ -8,8 +8,8 @@ OpenShock.SDK.CSharp.Live OpenShock.SDK.CSharp.Live OpenShock - 0.0.24 - 0.0.24 + 0.0.25 + 0.0.25 SDK.DotNet.Live OpenShock Extension for OpenShock.SDK.CSharp