-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
265 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters