From 52baa2f4946500782ec3c15bfe5aa4f6871a196f Mon Sep 17 00:00:00 2001 From: Brendan Westley Date: Mon, 30 Sep 2024 01:59:38 -0500 Subject: [PATCH] Implement PMS component --- Basestation_Software.Web/ColorGradient.cs | 68 +++++++ .../Core/Components/PMS.razor | 186 ++++++++++++++++++ Basestation_Software.Web/Core/Pages/RED.razor | 5 + Basestation_Software.sln | 12 +- 4 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 Basestation_Software.Web/ColorGradient.cs create mode 100644 Basestation_Software.Web/Core/Components/PMS.razor diff --git a/Basestation_Software.Web/ColorGradient.cs b/Basestation_Software.Web/ColorGradient.cs new file mode 100644 index 0000000..cea79bd --- /dev/null +++ b/Basestation_Software.Web/ColorGradient.cs @@ -0,0 +1,68 @@ +using OpenCvSharp; +using System.Drawing; + +namespace Basestation_Software.Web; + +public class ColorGradient +{ + SortedDictionary Stops { get; } + double MinPosition { get; } + double MaxPosition { get; } + + ColorGradient(Dictionary Stops) + { + if (Stops.Count < 2) throw new ArgumentException("Stops must have at least two elements"); + this.Stops = new SortedDictionary(Stops); + MinPosition = Stops.Keys.First(); + MaxPosition = Stops.Keys.Last(); + } + + public static double Map(double x, double in_min, double in_max, double out_min, double out_max) + { + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; + } + + public static int Map(double x, double in_min, double in_max, int out_min, int out_max) + { + return (int)((x - in_min) / (in_max - in_min) * (out_max - out_min)) + out_min; + } + + public static string ClampMap(double x, double in_min, double in_max, Color out_min, Color out_max) + { + if (x < in_min) x = in_min; + else if (x > in_max) x = in_max; + return Map(x, in_min, in_max, out_min, out_max); + } + + public static string Map(double x, double in_min, double in_max, Color out_min, Color out_max) + { + if (in_min == in_max) return ColorToString(out_min); // Prevent division by 0. + // Gamma correct by squaring the color before interpolating and square rooting after. + // This eliminates the horrible grey/brown sludge in the middle of two blended distant colors. https://youtu.be/LKnqECcg6Gw + return "rgb(" + + Math.Clamp(Math.Sqrt(Map(x, in_min, in_max, (double)out_min.R * out_min.R, (double)out_max.R * out_max.R)), 0, 255) + " " + + Math.Clamp(Math.Sqrt(Map(x, in_min, in_max, (double)out_min.G * out_min.G, (double)out_max.G * out_max.G)), 0, 255) + " " + + Math.Clamp(Math.Sqrt(Map(x, in_min, in_max, (double)out_min.B * out_min.B, (double)out_max.B * out_max.B)), 0, 255) + " / " + + Math.Clamp(Math.Sqrt(Map(x, in_min, in_max, (double)out_min.A * out_min.A, (double)out_max.A * out_max.A)), 0, 255) + ")"; + } + + public static string ColorToString(Color color) { return $"rgb({color.R}, {color.G}, {color.B} / {color.A})"; } + + public string this[double position] + { + get + { + // Unoptimized + if (position <= MinPosition) return ColorToString(Stops[MinPosition]); + double leftPosition = MinPosition; + Color leftColor = Stops[MinPosition]; + foreach (var (rightPosition, rightColor) in Stops) + { + if (position <= rightPosition) return Map(position, leftPosition, rightPosition, leftColor, rightColor); + leftPosition = rightPosition; + leftColor = rightColor; + } + return ColorToString(leftColor); + } + } +} diff --git a/Basestation_Software.Web/Core/Components/PMS.razor b/Basestation_Software.Web/Core/Components/PMS.razor new file mode 100644 index 0000000..20f5eb2 --- /dev/null +++ b/Basestation_Software.Web/Core/Components/PMS.razor @@ -0,0 +1,186 @@ +@implements IDisposable +@inject RoveCommService _RoveCommService +@inject IJSRuntime _IJSRuntime + + + +
+
+
PMS
+
+
+

Motors

+

Core

+

Aux

+

Core Current@($"{PackCurrent - AuxCurrent:0.00}") A

+

Aux Current@($"{AuxCurrent:0.00}") A

+

Pack Current@($"{PackCurrent:0.00}") A

+

CS1@($"{MiscCurrent[0]:0.00}") A

+

CS2@($"{MiscCurrent[1]:0.00}") A

+

CS3@($"{MiscCurrent[2]:0.00}") A

+

C1@($"{CellVoltage[0]:0.00}") V

+

C2@($"{CellVoltage[1]:0.00}") V

+

C3@($"{CellVoltage[2]:0.00}") V

+

C4@($"{CellVoltage[3]:0.00}") V

+

C5@($"{CellVoltage[4]:0.00}") V

+

C6@($"{CellVoltage[5]:0.00}") V

+

Pack Voltage@($"{PackVoltage:0.00}") V

+ + + +
+
+ +@code +{ + enum BusState + { + DISABLED, + ENABLED, + UNKNOWN, + } + + // Constants + public const uint TELEMETRY_TIMEOUT = 10_000; // ms + public static Color COLOR_ERROR = Color.FromArgb(255, 255, 0, 0); + public static Color COLOR_OK = Color.FromArgb(255, 0, 255, 0); + + // State + private List BusStatus = new List() { BusState.UNKNOWN, BusState.UNKNOWN, BusState.UNKNOWN }; // Motors, Core, Aux + private float PackCurrent = 0, PackVoltage = 0, AuxCurrent = 0; + private List CellVoltage = new List() { 0, 0, 0, 0, 0, 0 }; // C1 ... C6 + private List MiscCurrent = new List() { 0, 0, 0 }; // CS1 ... CS3 + private System.Threading.Timer TelemetryWatchdog; + + protected override void OnInitialized() + { + TelemetryWatchdog = new System.Threading.Timer(WatchdogTimeout, null, TELEMETRY_TIMEOUT, Timeout.Infinite); + + _RoveCommService.On("PMS", "PackCurrent", async (RoveCommPacket packet) => + { + PackCurrent = packet.Data[0]; + ResetWatchdog(); + await InvokeAsync(StateHasChanged); + }); + _RoveCommService.On("PMS", "PackVoltage", async (RoveCommPacket packet) => + { + PackVoltage = packet.Data[0]; + ResetWatchdog(); + await InvokeAsync(StateHasChanged); + }); + _RoveCommService.On("PMS", "CellVoltage", async (RoveCommPacket packet) => + { + CellVoltage = packet.Data; + ResetWatchdog(); + await InvokeAsync(StateHasChanged); + }); + _RoveCommService.On("PMS", "AuxCurrent", async (RoveCommPacket packet) => + { + AuxCurrent = packet.Data[0]; + ResetWatchdog(); + await InvokeAsync(StateHasChanged); + }); + _RoveCommService.On("PMS", "MiscCurrent", async (RoveCommPacket packet) => + { + MiscCurrent = packet.Data; + ResetWatchdog(); + await InvokeAsync(StateHasChanged); + }); + _RoveCommService.On("PMS", "PackVoltage", async (RoveCommPacket packet) => + { + PackVoltage = packet.Data[0]; + ResetWatchdog(); + await InvokeAsync(StateHasChanged); + }); + _RoveCommService.On("PMS", "BusStatus", async (RoveCommPacket packet) => + { + for (int i = 0; i < 3; i++) + BusStatus[i] = (packet.Data[0] & (1 << i)) == 0 ? BusState.DISABLED : BusState.ENABLED; + ResetWatchdog(); + await InvokeAsync(StateHasChanged); + }); + } + + async void WatchdogTimeout(object? _) + { + AuxCurrent = 0; + PackCurrent = 0; + PackVoltage = 0; + CellVoltage = [0, 0, 0, 0, 0, 0]; + BusStatus = [BusState.UNKNOWN, BusState.UNKNOWN, BusState.UNKNOWN]; + MiscCurrent = [0, 0, 0]; + await InvokeAsync(StateHasChanged); + } + + void ResetWatchdog() + { + TelemetryWatchdog.Change(TELEMETRY_TIMEOUT, Timeout.Infinite); + } + + async Task toggleBus(int i) + { + if (BusStatus[i] == BusState.DISABLED) + await _RoveCommService.SendAsync("PMS", "EnableBus", [(byte)(1 << i)]); + else if (BusStatus[i] == BusState.ENABLED) + await _RoveCommService.SendAsync("PMS", "DisableBus", [(byte)(1 << i)]); + BusStatus[i] = BusState.UNKNOWN; + } + + async Task Reboot() + { + if (await _IJSRuntime.InvokeAsync("confirm", "Are you sure you want to do this? It will take a few minutes to reboot the network switch.")) + await _RoveCommService.SendAsync("PMS", "Reboot", [1]); + } + + async Task EStop() + { + await _RoveCommService.SendAsync("PMS", "EStop", [1]); + } + + async Task Suicide() + { + if (await _IJSRuntime.InvokeAsync("confirm", "Are you sure you want to do this? You must pull the physical E Stop to turn the rover on again!")) + await _RoveCommService.SendAsync("PMS", "Suicide", [1]); + } + + void IDisposable.Dispose() { } +} \ No newline at end of file diff --git a/Basestation_Software.Web/Core/Pages/RED.razor b/Basestation_Software.Web/Core/Pages/RED.razor index 3b4cb63..bb7ec8e 100644 --- a/Basestation_Software.Web/Core/Pages/RED.razor +++ b/Basestation_Software.Web/Core/Pages/RED.razor @@ -26,6 +26,11 @@ @* *@ +
+
+ +
+
diff --git a/Basestation_Software.sln b/Basestation_Software.sln index 4bf6a80..4f65d98 100644 --- a/Basestation_Software.sln +++ b/Basestation_Software.sln @@ -3,20 +3,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basestation_Software.Web", "Basestation_Software.Web\Basestation_Software.Web.csproj", "{685AF553-D781-42C7-826A-34FC743DE774}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basestation_Software.Web", "Basestation_Software.Web\Basestation_Software.Web.csproj", "{685AF553-D781-42C7-826A-34FC743DE774}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basestation_Software.Models", "Basestation_Software.Models\Basestation_Software.Models.csproj", "{77205455-D724-4ABA-885D-56584808BD4A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basestation_Software.Models", "Basestation_Software.Models\Basestation_Software.Models.csproj", "{77205455-D724-4ABA-885D-56584808BD4A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basestation_Software.Api", "Basestation_Software.Api\Basestation_Software.Api.csproj", "{1E94F4E9-944B-4373-96E8-2B15C550A4AF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basestation_Software.Api", "Basestation_Software.Api\Basestation_Software.Api.csproj", "{1E94F4E9-944B-4373-96E8-2B15C550A4AF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {685AF553-D781-42C7-826A-34FC743DE774}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {685AF553-D781-42C7-826A-34FC743DE774}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -31,4 +28,7 @@ Global {1E94F4E9-944B-4373-96E8-2B15C550A4AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {1E94F4E9-944B-4373-96E8-2B15C550A4AF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection EndGlobal