Skip to content

Commit

Permalink
Implement PMS component
Browse files Browse the repository at this point in the history
  • Loading branch information
bwestley committed Sep 30, 2024
1 parent b35fdc7 commit 52baa2f
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 6 deletions.
68 changes: 68 additions & 0 deletions Basestation_Software.Web/ColorGradient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using OpenCvSharp;
using System.Drawing;

namespace Basestation_Software.Web;

public class ColorGradient
{
SortedDictionary<double, Color> Stops { get; }
double MinPosition { get; }
double MaxPosition { get; }

ColorGradient(Dictionary<double, Color> Stops)
{
if (Stops.Count < 2) throw new ArgumentException("Stops must have at least two elements");
this.Stops = new SortedDictionary<double, Color>(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);
}
}
}
186 changes: 186 additions & 0 deletions Basestation_Software.Web/Core/Components/PMS.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
@implements IDisposable
@inject RoveCommService _RoveCommService
@inject IJSRuntime _IJSRuntime

<style>
.pms {
display: grid;
padding: 2px; /* Grid exterior border width */
grid-template-columns: 2fr 1fr 1fr 2fr 3fr;
grid-template-areas:
"motors motors core-current core-current pack-current"
"core core core-current core-current pack-current"
"aux aux aux-current aux-current pack-current"
"cs1-current cs1-current cs2-current cs2-current cs3-current"
"c1-voltage c2-voltage c2-voltage c3-voltage pack-voltage"
"c4-voltage c5-voltage c5-voltage c6-voltage pack-voltage"
"reboot reboot e-stop e-stop suicide";
place-items: stretch stretch;
color: #000;
gap: 2px; /* Grid interior border width */
background-color: #000; /* Grid border color */
font-weight: bold;
}
.pms > p {
margin: 0;
padding: 0px 20px 0px 20px;
align-content: center;
}
.pms > p > span {
margin: 0;
float: right;
}
.pms > p > button {
margin: 0;
float: right;
}
.pms > button {
margin: 0;
}
</style>

<div class="card full-height">
<div class="card-header">
<h5 class="mr-auto">PMS</h5>
</div>
<div class="card-body pms w-100">
<p style="grid-area: motors; background-color: @(BusStatus[0] == BusState.ENABLED ? "yellow" : "grey")">Motors<button @onclick="() => toggleBus(0)">@(BusStatus[0] == BusState.ENABLED ? "Disable" : (BusStatus[0] == BusState.DISABLED ? "Enable" : "Pending..."))</button></p>
<p style="grid-area: core; background-color: @(BusStatus[1] == BusState.ENABLED ? "yellow" : "grey")">Core<button @onclick="() => toggleBus(1)">@(BusStatus[1] == BusState.ENABLED ? "Disable" : (BusStatus[1] == BusState.DISABLED ? "Enable" : "Pending..."))</button></p>
<p style="grid-area: aux; background-color: @(BusStatus[2] == BusState.ENABLED ? "yellow" : "grey")">Aux<button @onclick="() => toggleBus(2)">@(BusStatus[2] == BusState.ENABLED ? "Disable" : (BusStatus[2] == BusState.DISABLED ? "Enable" : "Pending..."))</button></p>
<p style="grid-area: core-current; background-color: @ColorGradient.ClampMap(PackCurrent - AuxCurrent, 7.2, 30, COLOR_OK, COLOR_ERROR)">Core Current<span>@($"{PackCurrent - AuxCurrent:0.00}") A</span></p>
<p style="grid-area: aux-current; background-color: @ColorGradient.ClampMap(AuxCurrent, 5, 15, COLOR_OK, COLOR_ERROR)">Aux Current<span>@($"{AuxCurrent:0.00}") A</span></p>
<p style="grid-area: pack-current; background-color: @ColorGradient.ClampMap(PackCurrent, 9.5, 45, COLOR_OK, COLOR_ERROR)">Pack Current<span>@($"{PackCurrent:0.00}") A</span></p>
<p style="grid-area: cs1-current; background-color: #fff">CS1<span>@($"{MiscCurrent[0]:0.00}") A</span></p>
<p style="grid-area: cs2-current; background-color: #fff">CS2<span>@($"{MiscCurrent[1]:0.00}") A</span></p>
<p style="grid-area: cs3-current; background-color: #fff">CS3<span>@($"{MiscCurrent[2]:0.00}") A</span></p>
<p style="grid-area: c1-voltage; background-color: @ColorGradient.ClampMap(CellVoltage[0], 2.5, 4.2, COLOR_ERROR, COLOR_OK)">C1<span>@($"{CellVoltage[0]:0.00}") V</span></p>
<p style="grid-area: c2-voltage; background-color: @ColorGradient.ClampMap(CellVoltage[1], 2.5, 4.2, COLOR_ERROR, COLOR_OK)">C2<span>@($"{CellVoltage[1]:0.00}") V</span></p>
<p style="grid-area: c3-voltage; background-color: @ColorGradient.ClampMap(CellVoltage[2], 2.5, 4.2, COLOR_ERROR, COLOR_OK)">C3<span>@($"{CellVoltage[2]:0.00}") V</span></p>
<p style="grid-area: c4-voltage; background-color: @ColorGradient.ClampMap(CellVoltage[3], 2.5, 4.2, COLOR_ERROR, COLOR_OK)">C4<span>@($"{CellVoltage[3]:0.00}") V</span></p>
<p style="grid-area: c5-voltage; background-color: @ColorGradient.ClampMap(CellVoltage[4], 2.5, 4.2, COLOR_ERROR, COLOR_OK)">C5<span>@($"{CellVoltage[4]:0.00}") V</span></p>
<p style="grid-area: c6-voltage; background-color: @ColorGradient.ClampMap(CellVoltage[5], 2.5, 4.2, COLOR_ERROR, COLOR_OK)">C6<span>@($"{CellVoltage[5]:0.00}") V</span></p>
<p style="grid-area: pack-voltage; background-color: @ColorGradient.ClampMap(PackVoltage, 15, 25, COLOR_ERROR, COLOR_OK)">Pack Voltage<span>@($"{PackVoltage:0.00}") V</span></p>
<button style="grid-area: reboot" onclick="@Reboot">REBOOT</button>
<button style="grid-area: e-stop" onclick="@EStop">E-STOP</button>
<button style="grid-area: suicide" onclick="@Suicide">SUICIDE</button>
</div>
</div>

@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<BusState> BusStatus = new List<BusState>() { BusState.UNKNOWN, BusState.UNKNOWN, BusState.UNKNOWN }; // Motors, Core, Aux
private float PackCurrent = 0, PackVoltage = 0, AuxCurrent = 0;
private List<float> CellVoltage = new List<float>() { 0, 0, 0, 0, 0, 0 }; // C1 ... C6
private List<float> MiscCurrent = new List<float>() { 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<float>("PMS", "PackCurrent", async (RoveCommPacket<float> packet) =>
{
PackCurrent = packet.Data[0];
ResetWatchdog();
await InvokeAsync(StateHasChanged);
});
_RoveCommService.On<float>("PMS", "PackVoltage", async (RoveCommPacket<float> packet) =>
{
PackVoltage = packet.Data[0];
ResetWatchdog();
await InvokeAsync(StateHasChanged);
});
_RoveCommService.On<float>("PMS", "CellVoltage", async (RoveCommPacket<float> packet) =>
{
CellVoltage = packet.Data;
ResetWatchdog();
await InvokeAsync(StateHasChanged);
});
_RoveCommService.On<float>("PMS", "AuxCurrent", async (RoveCommPacket<float> packet) =>
{
AuxCurrent = packet.Data[0];
ResetWatchdog();
await InvokeAsync(StateHasChanged);
});
_RoveCommService.On<float>("PMS", "MiscCurrent", async (RoveCommPacket<float> packet) =>
{
MiscCurrent = packet.Data;
ResetWatchdog();
await InvokeAsync(StateHasChanged);
});
_RoveCommService.On<float>("PMS", "PackVoltage", async (RoveCommPacket<float> packet) =>
{
PackVoltage = packet.Data[0];
ResetWatchdog();
await InvokeAsync(StateHasChanged);
});
_RoveCommService.On<byte>("PMS", "BusStatus", async (RoveCommPacket<byte> 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<byte>("PMS", "EnableBus", [(byte)(1 << i)]);
else if (BusStatus[i] == BusState.ENABLED)
await _RoveCommService.SendAsync<byte>("PMS", "DisableBus", [(byte)(1 << i)]);
BusStatus[i] = BusState.UNKNOWN;
}

async Task Reboot()
{
if (await _IJSRuntime.InvokeAsync<bool>("confirm", "Are you sure you want to do this? It will take a few minutes to reboot the network switch."))
await _RoveCommService.SendAsync<byte>("PMS", "Reboot", [1]);
}

async Task EStop()
{
await _RoveCommService.SendAsync<byte>("PMS", "EStop", [1]);
}

async Task Suicide()
{
if (await _IJSRuntime.InvokeAsync<bool>("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<byte>("PMS", "Suicide", [1]);
}

void IDisposable.Dispose() { }
}
5 changes: 5 additions & 0 deletions Basestation_Software.Web/Core/Pages/RED.razor
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
@* <CameraDisplay /> *@
</div>
</div>
<div class="row flex-grow-1">
<div class="col-12">
<PMS />
</div>
</div>
</div>
<div class="col-md-5 g-1">
<div class="row flex-grow-1">
Expand Down
12 changes: 6 additions & 6 deletions Basestation_Software.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

0 comments on commit 52baa2f

Please sign in to comment.