From 864eb8c89d742ed34d3dc4012e51c0dcd06d4f72 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Mon, 18 Nov 2024 00:22:47 +0100 Subject: [PATCH] Change logic for handling hub uptime, latency, and rssi --- API/Controller/Admin/GetOnlineDevices.cs | 12 ++--- .../TimeSpanToMillisecondsConverter.cs | 28 ---------- .../WebSocket/LCG/LatencyAnnounceData.cs | 4 +- Common/Models/WebSocket/LCG/PingResponse.cs | 6 --- Common/Redis/DeviceOnline.cs | 8 ++- .../Controllers/DeviceControllerBase.cs | 6 +-- .../Controllers/DeviceV1Controller.cs | 2 +- .../Controllers/DeviceV2Controller.cs | 28 ++++++---- .../Controllers/LiveControlController.cs | 52 +++++-------------- .../LifetimeManager/DeviceLifetime.cs | 30 +++++------ 10 files changed, 60 insertions(+), 116 deletions(-) delete mode 100644 Common/JsonSerialization/TimeSpanToMillisecondsConverter.cs delete mode 100644 Common/Models/WebSocket/LCG/PingResponse.cs diff --git a/API/Controller/Admin/GetOnlineDevices.cs b/API/Controller/Admin/GetOnlineDevices.cs index 4d91cf1f..8bdf6594 100644 --- a/API/Controller/Admin/GetOnlineDevices.cs +++ b/API/Controller/Admin/GetOnlineDevices.cs @@ -52,8 +52,8 @@ public async Task GetOnlineDevices() Name = dbItem.Name, ConnectedAt = x.ConnectedAt, UserAgent = x.UserAgent, - Uptime = x.Uptime, - Latency = x.Latency, + BootedAt = x.BootedAt, + LatencyMs = x.LatencyMs, Rssi = x.Rssi, }; }) @@ -73,10 +73,8 @@ public sealed class AdminOnlineDeviceResponse public required DateTimeOffset ConnectedAt { get; init; } public required string? UserAgent { get; init; } - [JsonConverter(typeof(TimeSpanToMillisecondsConverter))] - public required TimeSpan? Uptime { get; init; } - [JsonConverter(typeof(TimeSpanToMillisecondsConverter))] - public required TimeSpan? Latency { get; init; } - public required int Rssi { get; init; } + public required DateTimeOffset BootedAt { get; init; } + public required ushort? LatencyMs { get; init; } + public required int? Rssi { get; init; } } } \ No newline at end of file diff --git a/Common/JsonSerialization/TimeSpanToMillisecondsConverter.cs b/Common/JsonSerialization/TimeSpanToMillisecondsConverter.cs deleted file mode 100644 index f9b7d8b4..00000000 --- a/Common/JsonSerialization/TimeSpanToMillisecondsConverter.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace OpenShock.Common.JsonSerialization; - -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -public class TimeSpanToMillisecondsConverter : JsonConverter -{ - // Converts TimeSpan to JSON - public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) - { - // Convert TimeSpan to total milliseconds and write it as a JSON number - writer.WriteNumberValue(value.TotalMilliseconds); - } - - // Converts JSON to TimeSpan - public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.Number) - { - throw new JsonException("Expected number representing milliseconds."); - } - - // Read the milliseconds as a double and convert to TimeSpan - var milliseconds = reader.GetDouble(); - return TimeSpan.FromMilliseconds(milliseconds); - } -} diff --git a/Common/Models/WebSocket/LCG/LatencyAnnounceData.cs b/Common/Models/WebSocket/LCG/LatencyAnnounceData.cs index 83a5a2b3..5e681977 100644 --- a/Common/Models/WebSocket/LCG/LatencyAnnounceData.cs +++ b/Common/Models/WebSocket/LCG/LatencyAnnounceData.cs @@ -2,6 +2,6 @@ public sealed class LatencyAnnounceData { - public required ulong DeviceLatency { get; set; } - public required ulong OwnLatency { get; set; } + public required ushort DeviceLatency { get; set; } + public required ushort OwnLatency { get; set; } } \ No newline at end of file diff --git a/Common/Models/WebSocket/LCG/PingResponse.cs b/Common/Models/WebSocket/LCG/PingResponse.cs deleted file mode 100644 index c1acc43f..00000000 --- a/Common/Models/WebSocket/LCG/PingResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OpenShock.Common.Models.WebSocket.LCG; - -public sealed class PingResponse -{ - public required long Timestamp { get; set; } -} \ No newline at end of file diff --git a/Common/Redis/DeviceOnline.cs b/Common/Redis/DeviceOnline.cs index 51d01c1d..10ecd242 100644 --- a/Common/Redis/DeviceOnline.cs +++ b/Common/Redis/DeviceOnline.cs @@ -18,9 +18,7 @@ public sealed class DeviceOnline public required DateTimeOffset ConnectedAt { get; set; } public string? UserAgent { get; set; } = null; - [JsonConverter(typeof(TimeSpanToMillisecondsConverter))] - public TimeSpan? Uptime { get; set; } - [JsonConverter(typeof(TimeSpanToMillisecondsConverter))] - public TimeSpan? Latency { get; set; } - public int Rssi { get; set; } + public DateTimeOffset BootedAt { get; set; } + public ushort? LatencyMs { get; set; } + public int? Rssi { get; set; } } \ No newline at end of file diff --git a/LiveControlGateway/Controllers/DeviceControllerBase.cs b/LiveControlGateway/Controllers/DeviceControllerBase.cs index e4dd989f..1b7501a4 100644 --- a/LiveControlGateway/Controllers/DeviceControllerBase.cs +++ b/LiveControlGateway/Controllers/DeviceControllerBase.cs @@ -154,7 +154,7 @@ protected override async Task UnregisterConnection() /// /// Keep the device online /// - protected async Task SelfOnline(TimeSpan uptime, TimeSpan? latency = null, int rssi = -70) // -70dBm = OK connection + protected async Task SelfOnline(DateTimeOffset bootedAt, ushort? latency = null, int? rssi = null) { Logger.LogDebug("Received keep alive from device [{DeviceId}]", CurrentDevice.Id); @@ -168,8 +168,8 @@ protected async Task SelfOnline(TimeSpan uptime, TimeSpan? latency = null, int r FirmwareVersion = _firmwareVersion!, ConnectedAt = _connected, UserAgent = _userAgent, - Uptime = uptime, - Latency = latency, + BootedAt = bootedAt, + LatencyMs = latency, Rssi = rssi }); diff --git a/LiveControlGateway/Controllers/DeviceV1Controller.cs b/LiveControlGateway/Controllers/DeviceV1Controller.cs index b2e92e1b..557eea6c 100644 --- a/LiveControlGateway/Controllers/DeviceV1Controller.cs +++ b/LiveControlGateway/Controllers/DeviceV1Controller.cs @@ -64,7 +64,7 @@ protected override async Task Handle(HubToGatewayMessage data) switch (payload.Kind) { case HubToGatewayMessagePayload.ItemKind.KeepAlive: - await SelfOnline(TimeSpan.FromMilliseconds(payload.KeepAlive.Uptime)); + await SelfOnline(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(payload.KeepAlive.Uptime))); break; case HubToGatewayMessagePayload.ItemKind.OtaInstallStarted: diff --git a/LiveControlGateway/Controllers/DeviceV2Controller.cs b/LiveControlGateway/Controllers/DeviceV2Controller.cs index 09a62ef7..0248fdfd 100644 --- a/LiveControlGateway/Controllers/DeviceV2Controller.cs +++ b/LiveControlGateway/Controllers/DeviceV2Controller.cs @@ -1,4 +1,5 @@ -using Asp.Versioning; +using System.Diagnostics; +using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; @@ -27,7 +28,8 @@ public sealed class DeviceV2Controller : DeviceControllerBase _userHubContext; private readonly Timer _pingTimer; - private DateTimeOffset _lastPingSent = DateTimeOffset.UtcNow; + private long _pingTimestamp = Stopwatch.GetTimestamp(); + private ushort _latencyMs = 0; /// /// DI @@ -54,12 +56,12 @@ private async void PingTimerElapsed(object? state) { try { - _lastPingSent = DateTimeOffset.UtcNow; + _pingTimestamp = Stopwatch.GetTimestamp(); await QueueMessage(new GatewayToHubMessage { Payload = new GatewayToHubMessagePayload(new Ping { - UnixUtcTime = (ulong)_lastPingSent.ToUnixTimeSeconds() + UnixUtcTime = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), }) }); } @@ -68,11 +70,6 @@ await QueueMessage(new GatewayToHubMessage Logger.LogError(e, "Error while sending ping message to device [{DeviceId}]", CurrentDevice.Id); } } - - /// - /// The latency of the last ping - /// - public TimeSpan Latency { get; private set; } private OtaUpdateStatus? _lastStatus; @@ -90,8 +87,17 @@ protected override async Task Handle(HubToGatewayMessage data) switch (payload.Kind) { case HubToGatewayMessagePayload.ItemKind.Pong: - Latency = DateTimeOffset.UtcNow - _lastPingSent; - await SelfOnline(TimeSpan.FromMilliseconds(payload.Pong.Uptime), Latency, payload.Pong.Rssi); + + // Received pong without sending ping, this could be abusing the pong endpoint. + if (_pingTimestamp == 0) + { + // TODO: Kick or warn client. + return; + } + + _latencyMs = (ushort)Math.Min(Stopwatch.GetElapsedTime(_pingTimestamp).TotalMilliseconds, ushort.MaxValue); // If someone has a ping higher than 65 seconds, they are messing with us. Cap it to 65 seconds + _pingTimestamp = 0; + await SelfOnline(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(payload.Pong.Uptime)), _latencyMs, payload.Pong.Rssi); break; case HubToGatewayMessagePayload.ItemKind.OtaUpdateStarted: diff --git a/LiveControlGateway/Controllers/LiveControlController.cs b/LiveControlGateway/Controllers/LiveControlController.cs index 8b9b4bf9..1454aba9 100644 --- a/LiveControlGateway/Controllers/LiveControlController.cs +++ b/LiveControlGateway/Controllers/LiveControlController.cs @@ -1,4 +1,5 @@ -using System.Net.WebSockets; +using System.Diagnostics; +using System.Net.WebSockets; using System.Text.Json; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -55,17 +56,14 @@ public sealed class LiveControlController : WebsocketBaseController _sharedShockers = new(); private byte _tps = 10; + private long _pingTimestamp = Stopwatch.GetTimestamp(); + private ushort _latencyMs = 0; /// /// Connection Id for this connection, unique and random per connection /// public Guid ConnectionId => Guid.NewGuid(); - /// - /// Last latency in milliseconds, 0 initially - /// - public ulong LastLatency { get; private set; } = 0; - private readonly Timer _pingTimer = new(PingInterval); /// @@ -332,39 +330,19 @@ private Task ProcessResult(BaseRequest request) private async Task IntakePong(JsonDocument? requestData) { Logger.LogTrace("Intake pong"); - - var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - - PingResponse? pong; - try - { - pong = requestData.NewSlDeserialize(); - - if (pong == null) - { - Logger.LogWarning("Error while deserializing pong"); - await QueueMessage(new Common.Models.WebSocket.BaseResponse - { - ResponseType = LiveResponseType.InvalidData - }); - return; - } - } - catch (Exception e) + + // Received pong without sending ping, this could be abusing the pong endpoint. + if (_pingTimestamp == 0) { - Logger.LogWarning(e, "Error while deserializing frame"); - await QueueMessage(new Common.Models.WebSocket.BaseResponse - { - ResponseType = LiveResponseType.InvalidData - }); + // TODO: Kick or warn client. return; } - var latency = currentTimestamp - pong.Timestamp; - LastLatency = Convert.ToUInt64(Math.Max(0, latency)); + _latencyMs = (ushort)Math.Min(Stopwatch.GetElapsedTime(_pingTimestamp).TotalMilliseconds, ushort.MaxValue); // If someone has a ping higher than 65 seconds, they are messing with us. Cap it to 65 seconds + _pingTimestamp = 0; if (Logger.IsEnabled(LogLevel.Trace)) - Logger.LogTrace("Latency: {Latency}ms (raw: {RawLatency}ms)", LastLatency, latency); + Logger.LogTrace("Latency: {Latency}ms", _latencyMs); await QueueMessage(new Common.Models.WebSocket.BaseResponse { @@ -372,7 +350,7 @@ await QueueMessage(new Common.Models.WebSocket.BaseResponse Data = new LatencyAnnounceData { DeviceLatency = 0, // TODO: Implement device latency calculation - OwnLatency = LastLatency + OwnLatency = _latencyMs } }); } @@ -575,13 +553,11 @@ private async Task SendPing() Logger.LogDebug("Sending ping to live control user [{User}] for device [{Device}]", _currentUser.DbUser.Id, Id); + _pingTimestamp = Stopwatch.GetTimestamp(); await QueueMessage(new Common.Models.WebSocket.BaseResponse { ResponseType = LiveResponseType.Ping, - Data = new PingResponse - { - Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - } + Data = new object {} // No data for now }); } diff --git a/LiveControlGateway/LifetimeManager/DeviceLifetime.cs b/LiveControlGateway/LifetimeManager/DeviceLifetime.cs index 419714e5..24196ed9 100644 --- a/LiveControlGateway/LifetimeManager/DeviceLifetime.cs +++ b/LiveControlGateway/LifetimeManager/DeviceLifetime.cs @@ -248,9 +248,9 @@ await deviceOnline.InsertAsync(new DeviceOnline Gateway = data.Gateway, ConnectedAt = data.ConnectedAt, UserAgent = data.UserAgent, - Latency = data.Latency, + BootedAt = data.BootedAt, + LatencyMs = data.LatencyMs, Rssi = data.Rssi, - Uptime = data.Uptime }, Duration.DeviceKeepAliveTimeout); @@ -259,9 +259,9 @@ await deviceOnline.InsertAsync(new DeviceOnline } // We cannot rely on the json set anymore, since that also happens with uptime and latency - // as we dont want to send a device online status every time, we will do it here - online.Uptime = data.Uptime; - online.Latency = data.Latency; + // as we don't want to send a device online status every time, we will do it here + online.BootedAt = data.BootedAt; + online.LatencyMs = data.LatencyMs; online.Rssi = data.Rssi; var sendOnlineStatusUpdate = false; @@ -311,27 +311,27 @@ public readonly struct SelfOnlineData /// /// /// - /// + /// /// - /// + /// /// public SelfOnlineData( Guid owner, string gateway, SemVersion firmwareVersion, DateTimeOffset connectedAt, - TimeSpan uptime, string userAgent, - TimeSpan? latency = null, - int rssi = -70) + DateTimeOffset bootedAt, + ushort? latencyMs = null, + int? rssi = null) { Owner = owner; Gateway = gateway; FirmwareVersion = firmwareVersion; ConnectedAt = connectedAt; - Uptime = uptime; UserAgent = userAgent; - Latency = latency; + BootedAt = bootedAt; + LatencyMs = latencyMs; Rssi = rssi; } @@ -363,15 +363,15 @@ public SelfOnlineData( /// /// Hub uptime /// - public required TimeSpan Uptime { get; init; } + public DateTimeOffset BootedAt { get; init; } /// /// Measured latency /// - public TimeSpan? Latency { get; init; } = null; + public ushort? LatencyMs { get; init; } = null; /// /// Wifi rssi /// - public int Rssi { get; init; } = -70; + public int? Rssi { get; init; } = null; } \ No newline at end of file