Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -0,0 +1,299 @@
@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();
}