300 lines
12 KiB
Plaintext
300 lines
12 KiB
Plaintext
@using MudBlazor
|
|
@using Microsoft.AspNetCore.SignalR.Client
|
|
@using RobotNet10.RobotApp.Client.Clients
|
|
@using RobotNet10.Shared.Sensor
|
|
@implements IAsyncDisposable
|
|
@inject BatteryHubClient BatteryHubClient
|
|
@inject ISnackbar Snackbar
|
|
|
|
<MudCard>
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h5">@DeviceName</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">@DeviceId</MudText>
|
|
</CardHeaderContent>
|
|
<CardHeaderActions>
|
|
<MudTooltip Text="Reload battery data">
|
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
|
Color="Color.Primary"
|
|
Size="Size.Small"
|
|
OnClick="ReloadBatteryDataAsync"
|
|
Disabled="@(!BatteryHubClient.IsConnected || IsReloading)">
|
|
</MudIconButton>
|
|
</MudTooltip>
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
|
|
<MudCardContent Style="position: relative;">
|
|
<MudGrid>
|
|
|
|
<!-- LEFT COLUMN : SVG BATTERY -->
|
|
<MudItem xs="12" md="4">
|
|
<div class="d-flex flex-column align-center justify-center" style="height: 100%; width: 100%; position: relative;">
|
|
|
|
@* =================== SVG KHÔNG ĐỔI =================== *@
|
|
<svg width="100%" height="100%" viewBox="0 0 200 300" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" style="max-height: 100%;">
|
|
<defs>
|
|
<linearGradient id="@($"healthGradient_{DeviceId}_{(int)BatteryData.Percentage}")" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
<stop offset="0%" style="stop-color:@GetHealthColor();stop-opacity:0.95" />
|
|
<stop offset="50%" style="stop-color:@GetHealthColor();stop-opacity:0.85" />
|
|
<stop offset="100%" style="stop-color:@GetHealthColor();stop-opacity:0.75" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<rect x="40" y="20" width="120" height="200" rx="8" ry="8"
|
|
fill="none" stroke="currentColor" stroke-width="4"
|
|
style="color: var(--mud-palette-text-primary);" />
|
|
|
|
<rect x="80" y="0" width="40" height="20" rx="4" ry="4"
|
|
fill="currentColor"
|
|
style="color: var(--mud-palette-text-primary);" />
|
|
|
|
<clipPath id="batteryClip">
|
|
<rect x="42" y="22" width="116" height="196" rx="6" ry="6" />
|
|
</clipPath>
|
|
|
|
@* ==== Charge Level Fill ==== *@
|
|
@{
|
|
var clipHeight = 196.0;
|
|
var clipY = 22.0;
|
|
var clipBottom = clipY + clipHeight;
|
|
var healthHeight = BatteryData.Percentage / 100.0 * clipHeight;
|
|
var healthY = clipBottom - healthHeight;
|
|
}
|
|
|
|
<g clip-path="url(#batteryClip)">
|
|
<rect x="42" y="@healthY"
|
|
width="116" height="@healthHeight"
|
|
rx="6" ry="6"
|
|
fill="@($"url(#healthGradient_{DeviceId}_{(int)BatteryData.Percentage})")"
|
|
opacity="0.9" />
|
|
|
|
<ellipse cx="100" cy="@(healthY + 6)"
|
|
rx="40" ry="8"
|
|
fill="white" opacity="0.3">
|
|
<animate attributeName="cy" dur="3s" repeatCount="indefinite"
|
|
values="@(healthY + 6);@(healthY + 12);@(healthY + 6)" />
|
|
<animate attributeName="opacity" dur="3s" repeatCount="indefinite"
|
|
values="0.3;0.4;0.3" />
|
|
</ellipse>
|
|
</g>
|
|
|
|
<text x="100" y="140"
|
|
text-anchor="middle" dominant-baseline="middle"
|
|
font-size="48" font-weight="bold"
|
|
fill="@GetHealthTextColor()">
|
|
@((int)BatteryData.Percentage)%
|
|
</text>
|
|
|
|
<text x="100" y="180"
|
|
text-anchor="middle" dominant-baseline="middle"
|
|
font-size="16"
|
|
fill="currentColor"
|
|
style="color: var(--mud-palette-text-secondary);">
|
|
Charge Level
|
|
</text>
|
|
</svg>
|
|
</div>
|
|
</MudItem>
|
|
|
|
<!-- RIGHT COLUMN -->
|
|
<MudItem xs="12" md="8">
|
|
<MudGrid>
|
|
|
|
<!-- HEALTH -->
|
|
<MudItem xs="12" sm="6">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h4">@GetHealthPercentage()%</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Health (SOH)</MudText>
|
|
<MudProgressLinear Value="@GetHealthPercentage()" Color="@GetChargeLevelColor()" Class="mt-2" />
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- VOLTAGE -->
|
|
<MudItem xs="12" sm="6">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h4">@BatteryData.Voltage.ToString("F2") V</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Voltage</MudText>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- CURRENT -->
|
|
<MudItem xs="12" sm="6">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h4" Color="@GetCurrentColor()">@BatteryData.Current.ToString("F2") A</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Current</MudText>
|
|
<MudChip T="string" Color="@(IsCharging()? Color.Success: Color.Default)" Size="Size.Small" Class="mt-2">
|
|
@(IsCharging() ? "Charging" : "Discharging")
|
|
</MudChip>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- TEMPERATURE -->
|
|
@if (BatteryData.CellTemperature != null && BatteryData.CellTemperature.Length > 0)
|
|
{
|
|
<MudItem xs="12" sm="6">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h4">@BatteryData.CellTemperature[0].ToString("F1") °C</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Temperature</MudText>
|
|
</MudCard>
|
|
</MudItem>
|
|
}
|
|
|
|
<!-- REMAIN CAPACITY -->
|
|
@if (!double.IsNaN(BatteryData.Charge))
|
|
{
|
|
<MudItem xs="12" sm="6">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h4">@BatteryData.Charge.ToString("F2") Ah</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Remaining Capacity</MudText>
|
|
</MudCard>
|
|
</MudItem>
|
|
}
|
|
|
|
<!-- FULL CAPACITY -->
|
|
@if (!double.IsNaN(BatteryData.Capacity))
|
|
{
|
|
<MudItem xs="12" sm="6">
|
|
<MudCard Elevation="0" Class="pa-4">
|
|
<MudText Typo="Typo.h4">@BatteryData.Capacity.ToString("F2") Ah</MudText>
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Full Capacity</MudText>
|
|
</MudCard>
|
|
</MudItem>
|
|
}
|
|
|
|
<!-- TIMESTAMP -->
|
|
<MudItem xs="12">
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
|
Last Update: @BatteryData.Header.Stamp.ToString("yyyy-MM-dd HH:mm:ss")
|
|
</MudText>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudItem>
|
|
|
|
</MudGrid>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
|
|
<MudOverlay Visible="@IsLoading" Absolute="true">
|
|
<MudProgressCircular Indeterminate="true" Size="Size.Large" />
|
|
</MudOverlay>
|
|
|
|
@code {
|
|
[Parameter, EditorRequired]
|
|
public string DeviceId { get; set; } = string.Empty;
|
|
|
|
private string DeviceName { get; set; } = string.Empty;
|
|
|
|
private BatteryState BatteryData = new();
|
|
|
|
private bool IsLoading => !BatteryHubClient.IsConnected;
|
|
private bool IsReloading = false;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
=> await base.OnInitializedAsync();
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender && !string.IsNullOrEmpty(DeviceId))
|
|
await ConnectAsync();
|
|
|
|
await base.OnAfterRenderAsync(firstRender);
|
|
}
|
|
|
|
private async Task ConnectAsync()
|
|
{
|
|
try
|
|
{
|
|
await BatteryHubClient.StartAsync();
|
|
var deviceInfo = await BatteryHubClient.GetDeviceInfoAsync(DeviceId);
|
|
DeviceName = deviceInfo?.DeviceName ?? DeviceId;
|
|
|
|
BatteryData = await BatteryHubClient.GetBatteryDataAsync(DeviceId);
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to connect: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task DisconnectAsync()
|
|
{
|
|
try
|
|
{
|
|
await BatteryHubClient.StopAsync();
|
|
BatteryData = new BatteryState();
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to disconnect: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task ReloadBatteryDataAsync()
|
|
{
|
|
if (!BatteryHubClient.IsConnected || IsReloading)
|
|
return;
|
|
|
|
try
|
|
{
|
|
IsReloading = true;
|
|
StateHasChanged();
|
|
|
|
BatteryData = await BatteryHubClient.GetBatteryDataAsync(DeviceId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Failed to reload battery data: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
IsReloading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private Color GetChargeLevelColor() =>
|
|
GetHealthPercentage() switch
|
|
{
|
|
>= 70 => Color.Success,
|
|
>= 30 => Color.Warning,
|
|
_ => Color.Error
|
|
};
|
|
|
|
private Color GetCurrentColor() =>
|
|
IsCharging() ? Color.Success : Color.Info;
|
|
|
|
private string GetHealthColor() =>
|
|
BatteryData.Percentage switch
|
|
{
|
|
>= 70 => "#4caf50",
|
|
>= 30 => "#ff9800",
|
|
_ => "#f44336"
|
|
};
|
|
|
|
private bool IsCharging() =>
|
|
BatteryData.PowerSupplyStatus == BatteryState.PowerSupplyStatusCharging;
|
|
|
|
private double GetHealthPercentage()
|
|
{
|
|
return BatteryData.PowerSupplyHealth switch
|
|
{
|
|
BatteryState.PowerSupplyHealthGood => 100.0,
|
|
BatteryState.PowerSupplyHealthOverheat => 50.0,
|
|
BatteryState.PowerSupplyHealthDead => 0.0,
|
|
BatteryState.PowerSupplyHealthOvervoltage => 30.0,
|
|
BatteryState.PowerSupplyHealthUnspecifiedFailure => 20.0,
|
|
BatteryState.PowerSupplyHealthCold => 40.0,
|
|
_ => 0.0
|
|
};
|
|
}
|
|
|
|
private string GetHealthTextColor() =>
|
|
"var(--mud-palette-text-primary)";
|
|
|
|
public async ValueTask DisposeAsync()
|
|
=> await DisconnectAsync();
|
|
}
|